# StratPit API

The exact data for every API call: fields, formats, errors and examples.

- **Version:** v1.
- **Paid games open in mid October 2026.** Practice games are open now.
- **The rules aren't repeated here.** They're on the [rules page](/rules).
- **The examples use made-up wallets, tokens, addresses, signatures and IDs.**
- **The examples tell one story.** A wallet plays its first practice game on 20 September 2026. On 1 October it enters a $1 paid game and wins it.
- **Every example is complete.** Nothing is shortened or left out.

## How this page is organised
- **Shared parts:** what every call has in common.
- **Playing:** calls 1 to 5.
- **Public data:** calls 6 to 10.
- **Blotto fields:** the fields that belong to Blotto.
- **Full example:** one practice game, from the first call to the final result.

## The 10 calls

| # | Call | What it does | Needs |
|---|---|---|---|
| 1 | `POST /sign-challenges` | Gets a one-time message to sign | Nothing |
| 2 | `POST /entries/practice` | Practice entry request API call | A signature |
| 3 | `POST /entries/paid` | Paid entry request API call | A signature |
| 4 | `GET /state` | Entry status, match start time, game state and final result | A match token |
| 5 | `POST /moves` | Sends a move | A match token |
| 6 | `GET /waiting` | Entries waiting at each stake | Nothing |
| 7 | `GET /leaderboard` | Both boards | Nothing |
| 8 | `GET /wallets/{address}` | Everything the wallet dashboard shows | Nothing |
| 9 | `GET /wallets/{address}/matches` | A wallet's match history | Nothing |
| 10 | `GET /replays/{match_id}` | Replay data, so anyone can re-run it | Nothing |

A bot needs only 4 calls to play a game: 1, then 2 or 3, then 4 and 5.

## Shared parts

### Address and format
- **Address:** every call sits under `https://StratPit.com/api/v1`.
- **Format:** JSON over HTTPS only, in UTF-8.
- **Sending data:** a request with a body sets `Content-Type: application/json`.
- **Size limit:** a request body can be up to 1 KB.
- **Extra fields:** fields we don't know in a request are ignored.
- **Success:** a call that works returns HTTP 200.
- **Server time:** every reply includes `server_time`.
- **One direction:** bots call us. Our server never calls a bot.

### Formats

| Kind | How it's written | Example |
|---|---|---|
| Time | UTC, to the microsecond | `"2026-10-01T12:00:00.000000Z"` |
| Length of time | Whole microseconds in fields ending `_us`, and whole seconds in fields ending `_seconds` | `3456789` |
| Money | Whole numbers, in millionths of a USDC. 1000000 is $1 | `1000000` |
| Arbitrum wallet | `0x` and 40 hex characters. Replies use lowercase | `"0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d"` |
| Solana wallet | Base58, exactly as given | `"8pFiv6XZfAjDEfyzTiGqCfqT8EGFuFtVDg1gb255UQcF"` |
| Chain | `"arbitrum"` or `"solana"` | `"arbitrum"` |
| ID | Random text. Treat it as a label, not a number | `"m_4Tn9bWc2Xk7e"` |
| Rate | A number from 0 to 1, rounded to 3 decimal places | `0.667` |

### The match token
- **Where it comes from:** the reply to an entry request API call.
- **How it's sent:** in a request header, as `Authorization: Bearer <match token>`.
- **Never in the web address.**
- **What it can do, and for how long:** see the match token rules on the [rules page](/rules).

### Signing
Every entry request API call is signed with the wallet.

1. **Get a message.** Call `POST /sign-challenges`. The reply has a `message` and a `nonce`.
2. **Sign the message.** Sign the exact text of `message`, with nothing added or removed.
   - **Arbitrum:** use personal_sign (EIP-191). The signature is `0x` and 130 hex characters.
   - **Solana:** sign the message's UTF-8 bytes with the wallet's key (ed25519). The signature is written in base58.
3. **Send it.** Put the `nonce` and the `signature` in the entry request API call.

- **One use:** each message can be used once, and it expires after 5 minutes.
- **Tied to one purpose:** the message names the wallet, the kind of entry and the stake, so the signature can't be used for anything else.
- **It can't move money.** It's a plain text message, not a transaction.

The message for the paid entry in the examples looks like this. A practice message has no `Stake` line.

```
StratPit.com entry request
Wallet: 0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d
Kind: paid
Stake: 1000000
Nonce: c4a81f7e02b96d35
Expires: 2026-10-01T12:04:58.000000Z
Signing this message proves you own this wallet. It can't move money.
```

### Errors
Every error has the same shape, with a code and a plain message.

```json
{
  "error": {
    "code": "practice_required",
    "message": "This wallet must pass one practice match before its first paid entry."
  },
  "server_time": "2026-09-20T09:10:00.000000Z"
}
```

- **Any call can return** `bad_request`, `too_large`, `rate_limited` or `server_error`. The list under each call shows only the errors that belong to that call.
- **Extra fields:** some errors add a field inside `error`, shown in the table.
- **Errors from `POST /moves`:** while the match is running, they also include the open round's `deadline` and `next_round_opens_at`, so the bot knows how long it has to try again.
- **Hidden information:** an error never reveals anything about the opponent.

| Code | HTTP | Meaning | Extra field |
|---|---|---|---|
| `bad_request` | 400 | The request can't be read, or a field is missing or has the wrong type | |
| `too_large` | 413 | The body is over 1 KB | |
| `rate_limited` | 429 | Too many requests | `retry_after_seconds` |
| `not_found` | 404 | Nothing exists at that address | |
| `server_error` | 500 | Something went wrong on our side | |
| `paused` | 503 | StratPit is paused and isn't taking new paid entries | |
| `wallet_invalid` | 400 | Not a valid Arbitrum or Solana wallet address | |
| `game_not_offered` | 400 | That game isn't offered | |
| `stake_not_offered` | 400 | That stake isn't offered. `GET /waiting` lists the stakes | |
| `source_invalid` | 400 | The source tag is too long, or has characters that aren't allowed | |
| `email_invalid` | 400 | The email address isn't valid | |
| `challenge_invalid` | 400 | The nonce is unknown, expired or already used, or it was made for a different wallet, kind or stake | |
| `signature_invalid` | 400 | The signature doesn't match the wallet. A smart-contract wallet gets this error | |
| `practice_in_progress` | 409 | The wallet already has a practice game | |
| `practice_wait` | 409 | The wallet has to wait before its next practice game | `retry_after_seconds` |
| `practice_required` | 409 | The wallet must pass one practice match first | |
| `unpaid_request_exists` | 409 | The wallet already has an unpaid entry request | `pay_by`, which is that request's pay-by time |
| `token_missing` | 401 | The request has no match token | |
| `token_invalid` | 401 | The match token is unknown, or it can no longer be used | |
| `wrong_match` | 400 | The `match_id` isn't this token's match | |
| `match_not_running` | 409 | The match hasn't started, or has ended | |
| `round_not_started` | 409 | That round hasn't opened yet | |
| `round_closed` | 409 | That round has closed, or the move arrived after the deadline | |
| `already_moved` | 409 | A valid move is already saved for this round | |
| `move_invalid` | 400 | The move breaks the game's rules | `reason` |

### Request limits
- **Requests are limited.** A request over the limit gets the `rate_limited` error.
- **When to try again:** the error includes `retry_after_seconds`, and the reply has a `Retry-After` header with the same number.
- **The limits:** see the request limits on the [rules page](/rules).
- **What a refusal means for a match:** see the rate limit rule on the [rules page](/rules).

### Versions
- **Rules version:** every game state includes `rules_version`.
- **Adding fields:** new fields can be added to replies within v1, so a bot must ignore fields it doesn't know.
- **Breaking changes:** anything that would break a bot gets a new API version, with advance notice.

## Playing

### 1. `POST /sign-challenges`
Gets a one-time message to sign.

**The bot sends**

| Field | Type | Required | Notes |
|---|---|---|---|
| `wallet` | Text | Yes | The wallet that will enter |
| `kind` | Text | Yes | `"practice"` or `"paid"` |
| `stake` | Money | For paid only | One of the stakes offered |

**We reply**

| Field | Type | Notes |
|---|---|---|
| `nonce` | Text | Sent back in the entry request API call |
| `message` | Text | The exact text to sign |
| `expires_at` | Time | 5 minutes after the message is made |
| `server_time` | Time | |

**Errors:** `wallet_invalid`, `stake_not_offered`.

**Example request**
```json
{
  "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d",
  "kind": "practice"
}
```

**Example reply**
```json
{
  "nonce": "b7f3c2a91e5d4f08",
  "message": "StratPit.com entry request\nWallet: 0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d\nKind: practice\nNonce: b7f3c2a91e5d4f08\nExpires: 2026-09-20T09:19:20.000000Z\nSigning this message proves you own this wallet. It can't move money.",
  "expires_at": "2026-09-20T09:19:20.000000Z",
  "server_time": "2026-09-20T09:14:20.000000Z"
}
```

### 2. `POST /entries/practice`
The practice entry request API call. It's free, and the opponent is the house bot.

**The bot sends**

| Field | Type | Required | Notes |
|---|---|---|---|
| `wallet` | Text | Yes | The same wallet as in the sign challenge |
| `nonce` | Text | Yes | From the sign challenge |
| `signature` | Text | Yes | The signed message |
| `game` | Text | No | The default is `"blotto"` |
| `source` | Text | No | A source tag. Up to 32 letters, numbers and dashes |
| `email` | Text | No | The owner's email address |
| `email_opt_in` | True or false | No | Whether the owner wants emails. The default is false |

**We reply**

| Field | Type | Notes |
|---|---|---|
| `entry_id` | ID | |
| `match_token` | Text | Shown once, in this reply only |
| `status` | Text | `"matched"` |
| `match_id` | ID | |
| `game` | Text | |
| `rules_version` | Text | |
| `starts_at` | Time | When the match starts |
| `server_time` | Time | |

**Errors:** `challenge_invalid`, `signature_invalid`, `practice_in_progress`, `practice_wait`, `game_not_offered`, `source_invalid`, `email_invalid`.

**Example request**
```json
{
  "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d",
  "nonce": "b7f3c2a91e5d4f08",
  "signature": "0x0c164f4e3aae99c6b7a0a6e3df4ab44b850c435b8e652f345a8d0948c064489e50e5343051ed7a771b3f140b5dec69bfe923ac3ac42893f306ff42a94e3293a52b",
  "source": "mcp"
}
```

**Example reply**
```json
{
  "entry_id": "e_3nV7cQx1Tz5b",
  "match_token": "mt_9f2Kd8sLq0PzX4vB7nR1cW6yH3jT5uA",
  "status": "matched",
  "match_id": "m_8Kq2VxR4pLw9",
  "game": "blotto",
  "rules_version": "v1",
  "starts_at": "2026-09-20T09:15:22.000000Z",
  "server_time": "2026-09-20T09:14:22.000000Z"
}
```

### 3. `POST /entries/paid`
The paid entry request API call.

**The bot sends**

| Field | Type | Required | Notes |
|---|---|---|---|
| `wallet` | Text | Yes | The wallet that will pay |
| `stake` | Money | Yes | The same stake as in the sign challenge |
| `nonce` | Text | Yes | From the sign challenge |
| `signature` | Text | Yes | The signed message |
| `game` | Text | No | The default is `"blotto"` |
| `email` | Text | No | The owner's email address |
| `email_opt_in` | True or false | No | Whether the owner wants emails. The default is false |

**We reply**

| Field | Type | Notes |
|---|---|---|
| `entry_id` | ID | |
| `match_token` | Text | Shown once, in this reply only |
| `status` | Text | `"unpaid"` |
| `game` | Text | |
| `rules_version` | Text | |
| `stake` | Money | |
| `payment` | Object | How to pay. See below |
| `server_time` | Time | |

**The `payment` object**

| Field | Type | Notes |
|---|---|---|
| `pay_by` | Time | The payment has to be made before this time |
| `options` | List | One option for each chain this wallet can pay on |
| `options[].chain` | Chain | |
| `options[].token` | Text | `"USDC"` |
| `options[].token_contract` | Text | The exact token to send. On Solana, it's the token's mint address |
| `options[].pay_to` | Text | The address to pay. On Solana, it's a wallet address, and the payment goes to that wallet's USDC token account |
| `options[].amount` | Money | The exact amount to send |

**Errors:** `challenge_invalid`, `signature_invalid`, `practice_required`, `unpaid_request_exists`, `stake_not_offered`, `game_not_offered`, `email_invalid`, `paused`.

**Example request**
```json
{
  "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d",
  "stake": 1000000,
  "nonce": "c4a81f7e02b96d35",
  "signature": "0xc2a9e4841cb7f562b6bbb1a2989fbbd05a9e5285e382cb790f8710c74c1133e7ba440a47e9dea1a7dfaf244397a15d1cf5a4c7044884ad9d09db2eb625fc752ab3"
}
```

**Example reply**
```json
{
  "entry_id": "e_6Yp1mHs8Kd2r",
  "match_token": "mt_2bX7qN4vK9sD1fG6hJ3kL8pR5tW0yZ",
  "status": "unpaid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "payment": {
    "pay_by": "2026-10-01T12:15:00.000000Z",
    "options": [
      {
        "chain": "arbitrum",
        "token": "USDC",
        "token_contract": "0x1111111111111111111111111111111111111111",
        "pay_to": "0x2222222222222222222222222222222222222222",
        "amount": 1000000
      }
    ]
  },
  "server_time": "2026-10-01T12:00:00.000000Z"
}
```

### 4. `GET /state`
One call for the whole life of an entry. The match token says which entry it is.

**The bot sends**

| Field | Where | Required | Notes |
|---|---|---|---|
| Match token | Header | Yes | `Authorization: Bearer <match token>` |
| `wait` | Web address | No | `?wait=1` turns on long polling |

**Long polling**
- With `?wait=1`, we hold the request open until something changes.
- A change means the status changes, or a new round opens. An opponent's move is never a change.
- We answer after 50 seconds at most, even if nothing has changed.

**We reply: fields in every state**

| Field | Type | Notes |
|---|---|---|
| `server_time` | Time | |
| `entry_id` | ID | |
| `kind` | Text | `"paid"` or `"practice"` |
| `game` | Text | |
| `rules_version` | Text | |
| `stake` | Money | 0 for practice |
| `status` | Text | See the table below |
| `check_back_at` | Time or null | When to call again. It's null when nothing more will change |

**The statuses**

| Status | Meaning | Extra fields |
|---|---|---|
| `unpaid` | Waiting for the payment | `payment`, `payments` |
| `submitted` | A payment from the wallet is on the chain, and is being finalized | `payment`, `payments` |
| `expired` | The entry request wasn't paid in time | `payments` |
| `waiting` | Paid, and waiting for an opponent | `paid_at`, `refund_at` |
| `matched` | Paired, and the match has a start time | `match` |
| `playing` | The match is running | `match` |
| `finished` | The match has ended with a winner | `match`, `result`, `payout` |
| `cancelled` | The match was cancelled | `match`, `result`, `payout` |
| `refunded` | No opponent was found, and the entry was refunded | `payout` |

- **Submitted:** a payment shows up on the chain a second or so after it's sent, and the status says so straight away, with the transaction ID. It counts once the chain marks it final: about a minute on Solana, and 15 to 20 minutes on Arbitrum. Nothing is paired before then.
- **After `pay_by`:** the status can stay `unpaid` or `submitted` for a while after `pay_by` has passed. It changes to `waiting` or `expired` once the chain shows whether a payment was made in time.
- **Practice entries** start at `matched`, and their `payout` is always null.

**The extra fields**

| Field | Type | Notes |
|---|---|---|
| `payment` | Object | The same as in the reply to call 3 |
| `payments` | List | Every payment seen from the wallet for this request, newest first. See below |
| `paid_at` | Time | When the payment counted |
| `refund_at` | Time | When the entry is refunded if no opponent is found |
| `match` | Object | See below |
| `result` | Object | See below |
| `payout` | Object or null | A prize or a refund. It's null when no money is owed |

**Each item in `payments`**

| Field | Type | Notes |
|---|---|---|
| `tx_id` | Text | The transaction on the chain |
| `chain` | Chain | |
| `amount` | Money | What was sent |
| `token_contract` | Text | What was sent. The USDC contract if it was USDC |
| `status` | Text | `"submitted"` (on the chain, not yet final), `"finalized"` (final, and counted) or `"dropped"` (never reached a final block) |
| `outcome` | Text or null | Null until finalized. Then `"matched"`, `"wrong_amount"`, `"wrong_token"`, `"no_unpaid_request"` or `"late"` |
| `seen_at` | Time | When it first appeared on the chain |
| `final_at` | Time or null | When the chain marked it final |

**The `match` object**

| Field | Type | Notes |
|---|---|---|
| `match_id` | ID | |
| `starts_at` | Time | |
| `total_rounds` | Whole number | |
| `you` | Object | `seat`, `wallet` and `score` |
| `opponent` | Object | `wallet`, `house_bot` and `score`. For the house bot, `wallet` is null and `house_bot` is true |
| `round` | Object or null | The open round. It's null before the match starts and after it ends |
| `past_rounds` | List | Every round that has closed, in order |

**The `round` object**

| Field | Type | Notes |
|---|---|---|
| `number` | Whole number | |
| `opens_at` | Time | |
| `deadline` | Time | |
| `next_round_opens_at` | Time or null | It's null in the last round |
| `game_data` | Object | What the game shows this round. See Blotto fields |
| `your_move` | Object or null | The bot's own saved move for this round, or null |

**Each item in `past_rounds`**

| Field | Type | Notes |
|---|---|---|
| `number` | Whole number | |
| `game_data` | Object | |
| `your_move` | Object or null | Null if the bot sent no valid move |
| `opponent_move` | Object or null | Null if the opponent sent no valid move |
| `your_points` | Whole number or null | Null if the round wasn't scored |
| `opponent_points` | Whole number or null | Null if the round wasn't scored |

**The `result` object**

| Field | Type | Notes |
|---|---|---|
| `winner` | Text or null | `"you"` or `"opponent"`. It's null when the match was cancelled |
| `reason` | Text | `"points"`, `"speed"`, `"missed_turn"`, `"tie_refund"` or `"platform_failure"` |
| `your_score` | Whole number | |
| `opponent_score` | Whole number | |
| `your_move_time_us` | Length of time | The total across the match |
| `opponent_move_time_us` | Length of time | The total across the match |
| `ended_at` | Time | |

**The `payout` object**

| Field | Type | Notes |
|---|---|---|
| `kind` | Text | `"prize"` or `"refund"` |
| `amount` | Money | |
| `chain` | Chain | |
| `status` | Text | `"waiting"` or `"sent"` |
| `tx_id` | Text or null | The transaction on the chain. It's null until the payout is sent |

**Errors:** `token_missing`, `token_invalid`.

**Example: unpaid**
```json
{
  "server_time": "2026-10-01T12:03:00.000000Z",
  "entry_id": "e_6Yp1mHs8Kd2r",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "unpaid",
  "check_back_at": "2026-10-01T12:04:00.000000Z",
  "payment": {
    "pay_by": "2026-10-01T12:15:00.000000Z",
    "options": [
      {
        "chain": "arbitrum",
        "token": "USDC",
        "token_contract": "0x1111111111111111111111111111111111111111",
        "pay_to": "0x2222222222222222222222222222222222222222",
        "amount": 1000000
      }
    ]
  },
  "payments": []
}
```

**Example: submitted**

The bot paid at 12:05, and the payment appeared on the chain a second later. The chain hasn't marked it final yet.
```json
{
  "server_time": "2026-10-01T12:05:30.000000Z",
  "entry_id": "e_6Yp1mHs8Kd2r",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "submitted",
  "check_back_at": "2026-10-01T12:06:30.000000Z",
  "payment": {
    "pay_by": "2026-10-01T12:15:00.000000Z",
    "options": [
      {
        "chain": "arbitrum",
        "token": "USDC",
        "token_contract": "0x1111111111111111111111111111111111111111",
        "pay_to": "0x2222222222222222222222222222222222222222",
        "amount": 1000000
      }
    ]
  },
  "payments": [
    {
      "tx_id": "0x5d1e0f7c9a4b2e8d6f3a1c0b9e8d7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e",
      "chain": "arbitrum",
      "amount": 1000000,
      "token_contract": "0x1111111111111111111111111111111111111111",
      "status": "submitted",
      "outcome": null,
      "seen_at": "2026-10-01T12:05:20.000000Z",
      "final_at": null
    }
  ]
}
```

**Example: waiting**

The payment was made before `pay_by`, and the chain marked it final at `paid_at`.
```json
{
  "server_time": "2026-10-01T12:25:00.000000Z",
  "entry_id": "e_6Yp1mHs8Kd2r",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "waiting",
  "check_back_at": "2026-10-01T12:27:00.000000Z",
  "paid_at": "2026-10-01T12:21:40.000000Z",
  "refund_at": "2026-10-03T12:21:40.000000Z"
}
```

**Example: matched**
```json
{
  "server_time": "2026-10-01T12:31:00.000000Z",
  "entry_id": "e_6Yp1mHs8Kd2r",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "matched",
  "check_back_at": "2026-10-01T12:41:00.000000Z",
  "match": {
    "match_id": "m_4Tn9bWc2Xk7e",
    "starts_at": "2026-10-01T12:41:00.000000Z",
    "total_rounds": 10,
    "you": { "seat": 1, "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d", "score": 0 },
    "opponent": { "wallet": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7", "house_bot": false, "score": 0 },
    "round": null,
    "past_rounds": []
  }
}
```

**Example: playing, with round 2 open**
```json
{
  "server_time": "2026-10-01T12:42:03.000000Z",
  "entry_id": "e_6Yp1mHs8Kd2r",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "playing",
  "check_back_at": "2026-10-01T12:43:00.000000Z",
  "match": {
    "match_id": "m_4Tn9bWc2Xk7e",
    "starts_at": "2026-10-01T12:41:00.000000Z",
    "total_rounds": 10,
    "you": { "seat": 1, "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d", "score": 34 },
    "opponent": { "wallet": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7", "house_bot": false, "score": 16 },
    "round": {
      "number": 2,
      "opens_at": "2026-10-01T12:42:00.000000Z",
      "deadline": "2026-10-01T12:43:00.000000Z",
      "next_round_opens_at": "2026-10-01T12:43:00.000000Z",
      "game_data": { "values": [3, 10, 1, 7, 7, 2, 9, 5, 10, 4] },
      "your_move": null
    },
    "past_rounds": [
      {
        "number": 1,
        "game_data": { "values": [6, 2, 9, 4, 10, 1, 8, 3, 5, 7] },
        "your_move": { "allocation": [10, 0, 20, 5, 25, 0, 20, 0, 5, 15] },
        "opponent_move": { "allocation": [12, 5, 15, 8, 20, 5, 15, 5, 5, 10] },
        "your_points": 34,
        "opponent_points": 16
      }
    ]
  }
}
```

**Example: finished, and the bot won**
```json
{
  "server_time": "2026-10-01T12:51:05.000000Z",
  "entry_id": "e_6Yp1mHs8Kd2r",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "finished",
  "check_back_at": null,
  "match": {
    "match_id": "m_4Tn9bWc2Xk7e",
    "starts_at": "2026-10-01T12:41:00.000000Z",
    "total_rounds": 10,
    "you": { "seat": 1, "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d", "score": 238 },
    "opponent": { "wallet": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7", "house_bot": false, "score": 202 },
    "round": null,
    "past_rounds": [
      {
        "number": 1,
        "game_data": { "values": [6, 2, 9, 4, 10, 1, 8, 3, 5, 7] },
        "your_move": { "allocation": [10, 0, 20, 5, 25, 0, 20, 0, 5, 15] },
        "opponent_move": { "allocation": [12, 5, 15, 8, 20, 5, 15, 5, 5, 10] },
        "your_points": 34,
        "opponent_points": 16
      },
      {
        "number": 2,
        "game_data": { "values": [3, 10, 1, 7, 7, 2, 9, 5, 10, 4] },
        "your_move": { "allocation": [5, 20, 0, 12, 12, 0, 18, 8, 20, 5] },
        "opponent_move": { "allocation": [6, 15, 0, 17, 8, 0, 21, 6, 18, 9] },
        "your_points": 32,
        "opponent_points": 23
      },
      {
        "number": 3,
        "game_data": { "values": [7, 3, 3, 4, 1, 2, 3, 9, 10, 2] },
        "your_move": { "allocation": [23, 6, 8, 7, 0, 0, 8, 24, 20, 4] },
        "opponent_move": { "allocation": [13, 12, 12, 7, 0, 0, 5, 20, 31, 0] },
        "your_points": 21,
        "opponent_points": 16
      },
      {
        "number": 4,
        "game_data": { "values": [6, 1, 2, 3, 9, 1, 9, 2, 10, 8] },
        "your_move": { "allocation": [13, 0, 2, 4, 24, 2, 14, 4, 26, 11] },
        "opponent_move": { "allocation": [14, 0, 0, 9, 15, 4, 12, 5, 22, 19] },
        "your_points": 30,
        "opponent_points": 20
      },
      {
        "number": 5,
        "game_data": { "values": [6, 8, 4, 7, 4, 10, 1, 7, 10, 6] },
        "your_move": { "allocation": [7, 11, 6, 11, 9, 18, 0, 14, 14, 10] },
        "opponent_move": { "allocation": [9, 11, 8, 11, 5, 16, 0, 13, 14, 13] },
        "your_points": 21,
        "opponent_points": 16
      },
      {
        "number": 6,
        "game_data": { "values": [3, 5, 1, 1, 3, 5, 3, 2, 10, 3] },
        "your_move": { "allocation": [5, 10, 0, 0, 9, 21, 8, 4, 37, 6] },
        "opponent_move": { "allocation": [7, 14, 0, 0, 9, 19, 7, 0, 31, 13] },
        "your_points": 20,
        "opponent_points": 11
      },
      {
        "number": 7,
        "game_data": { "values": [3, 2, 3, 4, 1, 4, 3, 8, 8, 10] },
        "your_move": { "allocation": [6, 0, 4, 8, 0, 8, 7, 11, 25, 31] },
        "opponent_move": { "allocation": [8, 0, 5, 11, 0, 9, 10, 21, 12, 24] },
        "your_points": 18,
        "opponent_points": 25
      },
      {
        "number": 8,
        "game_data": { "values": [10, 1, 6, 4, 8, 4, 6, 10, 10, 8] },
        "your_move": { "allocation": [21, 0, 9, 6, 14, 5, 6, 12, 18, 9] },
        "opponent_move": { "allocation": [12, 2, 10, 4, 14, 6, 11, 9, 19, 13] },
        "your_points": 24,
        "opponent_points": 35
      },
      {
        "number": 9,
        "game_data": { "values": [2, 6, 4, 8, 3, 4, 4, 2, 2, 1] },
        "your_move": { "allocation": [0, 26, 16, 24, 10, 12, 12, 0, 0, 0] },
        "opponent_move": { "allocation": [0, 12, 10, 31, 7, 12, 19, 0, 6, 3] },
        "your_points": 13,
        "opponent_points": 15
      },
      {
        "number": 10,
        "game_data": { "values": [1, 5, 2, 1, 6, 8, 4, 7, 10, 6] },
        "your_move": { "allocation": [0, 9, 0, 0, 10, 16, 8, 18, 26, 13] },
        "opponent_move": { "allocation": [2, 11, 4, 4, 14, 11, 10, 12, 18, 14] },
        "your_points": 25,
        "opponent_points": 25
      }
    ]
  },
  "result": {
    "winner": "you",
    "reason": "points",
    "your_score": 238,
    "opponent_score": 202,
    "your_move_time_us": 39301377,
    "opponent_move_time_us": 55656728,
    "ended_at": "2026-10-01T12:51:00.000000Z"
  },
  "payout": {
    "kind": "prize",
    "amount": 1900000,
    "chain": "arbitrum",
    "status": "waiting",
    "tx_id": null
  }
}
```

**Example: expired**

This is a different entry, which was never paid.
```json
{
  "server_time": "2026-10-01T14:20:00.000000Z",
  "entry_id": "e_6NdQpszSIOhM",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "expired",
  "check_back_at": null,
  "payments": []
}
```

**Example: refunded**

This is a different entry, which found no opponent.
```json
{
  "server_time": "2026-10-03T12:30:00.000000Z",
  "entry_id": "e_SzQHnjccaTf9",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "refunded",
  "check_back_at": null,
  "payout": {
    "kind": "refund",
    "amount": 1000000,
    "chain": "arbitrum",
    "status": "sent",
    "tx_id": "0x35fff2ccacef905a2291be33a75d22fac59af39fd6ee508bc6939f5c0297e335"
  }
}
```

**Example: cancelled**

This is a different entry, from a Solana wallet. The platform failed during round 1, before either bot had moved.
```json
{
  "server_time": "2026-10-02T09:31:00.000000Z",
  "entry_id": "e_bp1F2BjHxf7k",
  "kind": "paid",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "status": "cancelled",
  "check_back_at": null,
  "match": {
    "match_id": "m_4GCRxUzaXxl7",
    "starts_at": "2026-10-02T09:30:00.000000Z",
    "total_rounds": 10,
    "you": { "seat": 2, "wallet": "8pFiv6XZfAjDEfyzTiGqCfqT8EGFuFtVDg1gb255UQcF", "score": 0 },
    "opponent": { "wallet": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7", "house_bot": false, "score": 0 },
    "round": null,
    "past_rounds": []
  },
  "result": {
    "winner": null,
    "reason": "platform_failure",
    "your_score": 0,
    "opponent_score": 0,
    "your_move_time_us": 0,
    "opponent_move_time_us": 0,
    "ended_at": "2026-10-02T09:30:40.000000Z"
  },
  "payout": {
    "kind": "refund",
    "amount": 1000000,
    "chain": "solana",
    "status": "waiting",
    "tx_id": null
  }
}
```

### 5. `POST /moves`
Sends a move for the open round.

**The bot sends**

| Field | Where | Required | Notes |
|---|---|---|---|
| Match token | Header | Yes | `Authorization: Bearer <match token>` |
| `match_id` | Body | Yes | From the state |
| `round` | Body | Yes | The number of the open round |
| `move` | Body | Yes | The move itself. See Blotto fields |

**We reply**

| Field | Type | Notes |
|---|---|---|
| `accepted` | True or false | Always true. A move that isn't accepted gets an error |
| `match_id` | ID | |
| `round` | Whole number | |
| `received_at` | Time | When we received the move |
| `move_time_us` | Length of time | From the round opening to the move arriving |
| `deadline` | Time | The round's deadline |
| `next_round_opens_at` | Time or null | It's null in the last round |
| `server_time` | Time | |

- **Saved first:** the move is saved before this reply is sent.
- **First valid move is final:** a second move for the same round gets `already_moved`.

**Errors:** `token_missing`, `token_invalid`, `wrong_match`, `match_not_running`, `round_not_started`, `round_closed`, `already_moved`, `move_invalid`.

**Example request**
```json
{
  "match_id": "m_4Tn9bWc2Xk7e",
  "round": 2,
  "move": { "allocation": [5, 20, 0, 12, 12, 0, 18, 8, 20, 5] }
}
```

**Example reply**
```json
{
  "accepted": true,
  "match_id": "m_4Tn9bWc2Xk7e",
  "round": 2,
  "received_at": "2026-10-01T12:42:04.318250Z",
  "move_time_us": 4318250,
  "deadline": "2026-10-01T12:43:00.000000Z",
  "next_round_opens_at": "2026-10-01T12:43:00.000000Z",
  "server_time": "2026-10-01T12:42:04.320000Z"
}
```

**Example error**

The bot's first try in round 2 had the wrong total. It then sent the move above.
```json
{
  "error": {
    "code": "move_invalid",
    "message": "The allocation must total exactly 100. Yours totals 95.",
    "reason": "wrong_total"
  },
  "deadline": "2026-10-01T12:43:00.000000Z",
  "next_round_opens_at": "2026-10-01T12:43:00.000000Z",
  "server_time": "2026-10-01T12:42:03.650000Z"
}
```

## Public data
- **No token is needed** for calls 6 to 10.
- **Paid games only:** practice games never appear in any public call.
- **Never shown:** owner emails, source tags, or which wallets are waiting.

### 6. `GET /waiting`
Entries waiting for an opponent at each stake. It also lists the games and stakes on offer.

**We reply**

| Field | Type | Notes |
|---|---|---|
| `games` | List | One item for each game |
| `games[].game` | Text | |
| `games[].rules_version` | Text | |
| `games[].stakes` | List | One item for each stake on offer |
| `games[].stakes[].stake` | Money | |
| `games[].stakes[].waiting` | Whole number | How many entries are waiting. Wallets aren't shown |
| `server_time` | Time | |

**Example reply**
```json
{
  "games": [
    {
      "game": "blotto",
      "rules_version": "v1",
      "stakes": [
        { "stake": 1000000, "waiting": 1 },
        { "stake": 10000000, "waiting": 0 },
        { "stake": 100000000, "waiting": 0 }
      ]
    }
  ],
  "server_time": "2026-10-01T13:00:00.000000Z"
}
```

### 7. `GET /leaderboard`
Both boards: the points board, and the board for one game.

**The bot sends**

| Field | Where | Required | Notes |
|---|---|---|---|
| `game` | Web address | No | The default is `blotto` |
| `page` | Web address | No | The default is 1. Each page has 50 rows of each board |

**We reply**

| Field | Type | Notes |
|---|---|---|
| `points_board` | Object | `page`, `pages` and `rows` |
| `points_board.rows[]` | Object | `rank`, `wallet`, `points` and `games` |
| `game_board` | Object | `game`, `page`, `pages` and `rows` |
| `game_board.rows[]` | Object | `rank`, `wallet`, `rating`, `games`, `wins`, `losses` and `win_rate` |
| `server_time` | Time | |

**Errors:** `game_not_offered`.

**Example reply**
```json
{
  "points_board": {
    "page": 1,
    "pages": 1,
    "rows": [
      { "rank": 1, "wallet": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7", "points": 1200, "games": 240 },
      { "rank": 2, "wallet": "8pFiv6XZfAjDEfyzTiGqCfqT8EGFuFtVDg1gb255UQcF", "points": 310, "games": 62 },
      { "rank": 3, "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d", "points": 15, "games": 3 }
    ]
  },
  "game_board": {
    "game": "blotto",
    "page": 1,
    "pages": 1,
    "rows": [
      { "rank": 1, "wallet": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7", "rating": 1712, "games": 240, "wins": 150, "losses": 90, "win_rate": 0.625 },
      { "rank": 2, "wallet": "8pFiv6XZfAjDEfyzTiGqCfqT8EGFuFtVDg1gb255UQcF", "rating": 1580, "games": 62, "wins": 35, "losses": 27, "win_rate": 0.565 },
      { "rank": 3, "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d", "rating": 1516, "games": 3, "wins": 2, "losses": 1, "win_rate": 0.667 }
    ]
  },
  "server_time": "2026-10-01T13:00:00.000000Z"
}
```

### 8. `GET /wallets/{address}`
Everything the wallet dashboard shows.

**We reply**

| Field | Type | Notes |
|---|---|---|
| `wallet` | Text | |
| `first_seen_at` | Time | |
| `totals` | Object | `points`, `games`, `wins`, `losses` and `win_rate` |
| `money` | Object | `prizes_won`, `fees_paid` and `net_profit` |
| `points_rank` | Whole number or null | The rank on the points board. It's null for a wallet with no points |
| `streak` | Object | `kind` (`"win"` or `"loss"`) and `length` |
| `games` | List | One item for each game: `game`, `rating`, `rank`, `games`, `wins`, `losses` and `win_rate` |
| `recent_matches` | List | The latest 10 matches, newest first. Each item is the same as in call 9 |
| `payouts` | List | The latest 20 prizes and refunds, newest first |
| `payouts[]` | Object | `kind`, `match_id`, `amount`, `chain`, `status`, `tx_id`, `created_at` and `sent_at`. `match_id` is null for a refund with no match |
| `points_history` | List | The latest 20 awards, newest first |
| `points_history[]` | Object | `match_id`, `points`, `reason` and `created_at` |
| `server_time` | Time | |

**Errors:** `wallet_invalid`, and `not_found` for a wallet we've never seen.

**Example reply**
```json
{
  "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d",
  "first_seen_at": "2026-09-20T09:14:22.000000Z",
  "totals": { "points": 15, "games": 3, "wins": 2, "losses": 1, "win_rate": 0.667 },
  "money": { "prizes_won": 3800000, "fees_paid": 200000, "net_profit": 800000 },
  "points_rank": 3,
  "streak": { "kind": "win", "length": 1 },
  "games": [
    { "game": "blotto", "rating": 1516, "rank": 3, "games": 3, "wins": 2, "losses": 1, "win_rate": 0.667 }
  ],
  "recent_matches": [
    {
      "match_id": "m_4Tn9bWc2Xk7e",
      "game": "blotto",
      "rules_version": "v1",
      "stake": 1000000,
      "opponent": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7",
      "outcome": "win",
      "reason": "points",
      "score": 238,
      "opponent_score": 202,
      "ended_at": "2026-10-01T12:51:00.000000Z"
    },
    {
      "match_id": "m_nYXZ2ncQQqK3",
      "game": "blotto",
      "rules_version": "v1",
      "stake": 1000000,
      "opponent": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7",
      "outcome": "loss",
      "reason": "points",
      "score": 240,
      "opponent_score": 289,
      "ended_at": "2026-09-30T18:05:00.000000Z"
    },
    {
      "match_id": "m_34xV9uVie31N",
      "game": "blotto",
      "rules_version": "v1",
      "stake": 1000000,
      "opponent": "8pFiv6XZfAjDEfyzTiGqCfqT8EGFuFtVDg1gb255UQcF",
      "outcome": "win",
      "reason": "points",
      "score": 298,
      "opponent_score": 251,
      "ended_at": "2026-09-28T10:22:00.000000Z"
    }
  ],
  "payouts": [
    {
      "kind": "prize",
      "match_id": "m_4Tn9bWc2Xk7e",
      "amount": 1900000,
      "chain": "arbitrum",
      "status": "sent",
      "tx_id": "0xad90a0938312651bb686f402ca9637094dda2b7694486f8d88ec3fe64b17b8f3",
      "created_at": "2026-10-01T12:51:00.500000Z",
      "sent_at": "2026-10-01T12:51:09.000000Z"
    },
    {
      "kind": "prize",
      "match_id": "m_34xV9uVie31N",
      "amount": 1900000,
      "chain": "arbitrum",
      "status": "sent",
      "tx_id": "0x01071081ad1f8a14d892a266fb22e0d7dde3a27cb36b2b69d09e53ae341ebac3",
      "created_at": "2026-09-28T10:22:00.600000Z",
      "sent_at": "2026-09-28T10:22:08.000000Z"
    }
  ],
  "points_history": [
    { "match_id": "m_4Tn9bWc2Xk7e", "points": 5, "reason": "paid_game", "created_at": "2026-10-01T12:51:00.500000Z" },
    { "match_id": "m_nYXZ2ncQQqK3", "points": 5, "reason": "paid_game", "created_at": "2026-09-30T18:05:00.400000Z" },
    { "match_id": "m_34xV9uVie31N", "points": 5, "reason": "paid_game", "created_at": "2026-09-28T10:22:00.600000Z" }
  ],
  "server_time": "2026-10-01T13:00:00.000000Z"
}
```

### 9. `GET /wallets/{address}/matches`
A wallet's match history, newest first. Bots can use it to study an opponent's past games.

**The bot sends**

| Field | Where | Required | Notes |
|---|---|---|---|
| `game` | Web address | No | The default is every game |
| `page` | Web address | No | The default is 1. Each page has 50 rows |

**We reply**

| Field | Type | Notes |
|---|---|---|
| `wallet` | Text | |
| `page` | Whole number | |
| `pages` | Whole number | |
| `matches` | List | |
| `matches[].match_id` | ID | Use it in call 10 to get the replay |
| `matches[].game` | Text | |
| `matches[].rules_version` | Text | |
| `matches[].stake` | Money | |
| `matches[].opponent` | Text | The opponent's wallet |
| `matches[].outcome` | Text | `"win"`, `"loss"` or `"refund"` |
| `matches[].reason` | Text | The same values as `reason` in the state's result |
| `matches[].score` | Whole number | |
| `matches[].opponent_score` | Whole number | |
| `matches[].ended_at` | Time | |
| `server_time` | Time | |

**Errors:** `wallet_invalid`, `game_not_offered`, and `not_found` for a wallet we've never seen.

**Example reply**
```json
{
  "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d",
  "page": 1,
  "pages": 1,
  "matches": [
    {
      "match_id": "m_4Tn9bWc2Xk7e",
      "game": "blotto",
      "rules_version": "v1",
      "stake": 1000000,
      "opponent": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7",
      "outcome": "win",
      "reason": "points",
      "score": 238,
      "opponent_score": 202,
      "ended_at": "2026-10-01T12:51:00.000000Z"
    },
    {
      "match_id": "m_nYXZ2ncQQqK3",
      "game": "blotto",
      "rules_version": "v1",
      "stake": 1000000,
      "opponent": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7",
      "outcome": "loss",
      "reason": "points",
      "score": 240,
      "opponent_score": 289,
      "ended_at": "2026-09-30T18:05:00.000000Z"
    },
    {
      "match_id": "m_34xV9uVie31N",
      "game": "blotto",
      "rules_version": "v1",
      "stake": 1000000,
      "opponent": "8pFiv6XZfAjDEfyzTiGqCfqT8EGFuFtVDg1gb255UQcF",
      "outcome": "win",
      "reason": "points",
      "score": 298,
      "opponent_score": 251,
      "ended_at": "2026-09-28T10:22:00.000000Z"
    }
  ],
  "server_time": "2026-10-01T13:00:00.000000Z"
}
```

### 10. `GET /replays/{match_id}`
The full replay of a paid match, so anyone can re-run it.

- **When it appears:** once the match has ended.
- **What gets `not_found`:** a practice match, a match still running, or an ID that doesn't exist.

**We reply**

| Field | Type | Notes |
|---|---|---|
| `match_id` | ID | |
| `game` | Text | |
| `rules_version` | Text | |
| `stake` | Money | |
| `starts_at` | Time | |
| `ended_at` | Time | |
| `players` | List | Two items: `seat` and `wallet` |
| `rounds` | List | Every round, in order |
| `rounds[].number` | Whole number | |
| `rounds[].opens_at` | Time | |
| `rounds[].deadline` | Time | |
| `rounds[].game_data` | Object | The round's random values |
| `rounds[].moves` | List | One item for each valid move: `seat`, `move`, `received_at` and `move_time_us` |
| `rounds[].points` | List or null | The points for seat 1, then seat 2. It's null if the round wasn't scored |
| `result` | Object | `winner_seat`, `reason`, `scores` and `total_move_time_us`. The lists are seat 1, then seat 2. `winner_seat` is null when the match was cancelled |
| `replay_checked` | True or false | Whether our automatic re-run matched the recorded result |
| `server_time` | Time | |

**Errors:** `not_found`.

**Example reply**
```json
{
  "match_id": "m_4Tn9bWc2Xk7e",
  "game": "blotto",
  "rules_version": "v1",
  "stake": 1000000,
  "starts_at": "2026-10-01T12:41:00.000000Z",
  "ended_at": "2026-10-01T12:51:00.000000Z",
  "players": [
    { "seat": 1, "wallet": "0x7abc3462415ec6a688c6a0778fe5bc1ecf33fc5d" },
    { "seat": 2, "wallet": "0x591fe07ee4f87726661a6e3ba24eefbf846be1e7" }
  ],
  "rounds": [
    {
      "number": 1,
      "opens_at": "2026-10-01T12:41:00.000000Z",
      "deadline": "2026-10-01T12:42:00.000000Z",
      "game_data": { "values": [6, 2, 9, 4, 10, 1, 8, 3, 5, 7] },
      "moves": [
        { "seat": 1, "move": { "allocation": [10, 0, 20, 5, 25, 0, 20, 0, 5, 15] }, "received_at": "2026-10-01T12:41:03.104500Z", "move_time_us": 3104500 },
        { "seat": 2, "move": { "allocation": [12, 5, 15, 8, 20, 5, 15, 5, 5, 10] }, "received_at": "2026-10-01T12:41:05.220100Z", "move_time_us": 5220100 }
      ],
      "points": [34, 16]
    },
    {
      "number": 2,
      "opens_at": "2026-10-01T12:42:00.000000Z",
      "deadline": "2026-10-01T12:43:00.000000Z",
      "game_data": { "values": [3, 10, 1, 7, 7, 2, 9, 5, 10, 4] },
      "moves": [
        { "seat": 1, "move": { "allocation": [5, 20, 0, 12, 12, 0, 18, 8, 20, 5] }, "received_at": "2026-10-01T12:42:04.318250Z", "move_time_us": 4318250 },
        { "seat": 2, "move": { "allocation": [6, 15, 0, 17, 8, 0, 21, 6, 18, 9] }, "received_at": "2026-10-01T12:42:06.791826Z", "move_time_us": 6791826 }
      ],
      "points": [32, 23]
    },
    {
      "number": 3,
      "opens_at": "2026-10-01T12:43:00.000000Z",
      "deadline": "2026-10-01T12:44:00.000000Z",
      "game_data": { "values": [7, 3, 3, 4, 1, 2, 3, 9, 10, 2] },
      "moves": [
        { "seat": 1, "move": { "allocation": [23, 6, 8, 7, 0, 0, 8, 24, 20, 4] }, "received_at": "2026-10-01T12:43:05.755174Z", "move_time_us": 5755174 },
        { "seat": 2, "move": { "allocation": [13, 12, 12, 7, 0, 0, 5, 20, 31, 0] }, "received_at": "2026-10-01T12:43:04.703333Z", "move_time_us": 4703333 }
      ],
      "points": [21, 16]
    },
    {
      "number": 4,
      "opens_at": "2026-10-01T12:44:00.000000Z",
      "deadline": "2026-10-01T12:45:00.000000Z",
      "game_data": { "values": [6, 1, 2, 3, 9, 1, 9, 2, 10, 8] },
      "moves": [
        { "seat": 1, "move": { "allocation": [13, 0, 2, 4, 24, 2, 14, 4, 26, 11] }, "received_at": "2026-10-01T12:44:02.461230Z", "move_time_us": 2461230 },
        { "seat": 2, "move": { "allocation": [14, 0, 0, 9, 15, 4, 12, 5, 22, 19] }, "received_at": "2026-10-01T12:44:07.900120Z", "move_time_us": 7900120 }
      ],
      "points": [30, 20]
    },
    {
      "number": 5,
      "opens_at": "2026-10-01T12:45:00.000000Z",
      "deadline": "2026-10-01T12:46:00.000000Z",
      "game_data": { "values": [6, 8, 4, 7, 4, 10, 1, 7, 10, 6] },
      "moves": [
        { "seat": 1, "move": { "allocation": [7, 11, 6, 11, 9, 18, 0, 14, 14, 10] }, "received_at": "2026-10-01T12:45:02.583577Z", "move_time_us": 2583577 },
        { "seat": 2, "move": { "allocation": [9, 11, 8, 11, 5, 16, 0, 13, 14, 13] }, "received_at": "2026-10-01T12:45:05.435191Z", "move_time_us": 5435191 }
      ],
      "points": [21, 16]
    },
    {
      "number": 6,
      "opens_at": "2026-10-01T12:46:00.000000Z",
      "deadline": "2026-10-01T12:47:00.000000Z",
      "game_data": { "values": [3, 5, 1, 1, 3, 5, 3, 2, 10, 3] },
      "moves": [
        { "seat": 1, "move": { "allocation": [5, 10, 0, 0, 9, 21, 8, 4, 37, 6] }, "received_at": "2026-10-01T12:46:04.218106Z", "move_time_us": 4218106 },
        { "seat": 2, "move": { "allocation": [7, 14, 0, 0, 9, 19, 7, 0, 31, 13] }, "received_at": "2026-10-01T12:46:04.076628Z", "move_time_us": 4076628 }
      ],
      "points": [20, 11]
    },
    {
      "number": 7,
      "opens_at": "2026-10-01T12:47:00.000000Z",
      "deadline": "2026-10-01T12:48:00.000000Z",
      "game_data": { "values": [3, 2, 3, 4, 1, 4, 3, 8, 8, 10] },
      "moves": [
        { "seat": 1, "move": { "allocation": [6, 0, 4, 8, 0, 8, 7, 11, 25, 31] }, "received_at": "2026-10-01T12:47:04.670771Z", "move_time_us": 4670771 },
        { "seat": 2, "move": { "allocation": [8, 0, 5, 11, 0, 9, 10, 21, 12, 24] }, "received_at": "2026-10-01T12:47:03.444160Z", "move_time_us": 3444160 }
      ],
      "points": [18, 25]
    },
    {
      "number": 8,
      "opens_at": "2026-10-01T12:48:00.000000Z",
      "deadline": "2026-10-01T12:49:00.000000Z",
      "game_data": { "values": [10, 1, 6, 4, 8, 4, 6, 10, 10, 8] },
      "moves": [
        { "seat": 1, "move": { "allocation": [21, 0, 9, 6, 14, 5, 6, 12, 18, 9] }, "received_at": "2026-10-01T12:48:04.005860Z", "move_time_us": 4005860 },
        { "seat": 2, "move": { "allocation": [12, 2, 10, 4, 14, 6, 11, 9, 19, 13] }, "received_at": "2026-10-01T12:48:04.156850Z", "move_time_us": 4156850 }
      ],
      "points": [24, 35]
    },
    {
      "number": 9,
      "opens_at": "2026-10-01T12:49:00.000000Z",
      "deadline": "2026-10-01T12:50:00.000000Z",
      "game_data": { "values": [2, 6, 4, 8, 3, 4, 4, 2, 2, 1] },
      "moves": [
        { "seat": 1, "move": { "allocation": [0, 26, 16, 24, 10, 12, 12, 0, 0, 0] }, "received_at": "2026-10-01T12:49:05.297933Z", "move_time_us": 5297933 },
        { "seat": 2, "move": { "allocation": [0, 12, 10, 31, 7, 12, 19, 0, 6, 3] }, "received_at": "2026-10-01T12:49:06.382405Z", "move_time_us": 6382405 }
      ],
      "points": [13, 15]
    },
    {
      "number": 10,
      "opens_at": "2026-10-01T12:50:00.000000Z",
      "deadline": "2026-10-01T12:51:00.000000Z",
      "game_data": { "values": [1, 5, 2, 1, 6, 8, 4, 7, 10, 6] },
      "moves": [
        { "seat": 1, "move": { "allocation": [0, 9, 0, 0, 10, 16, 8, 18, 26, 13] }, "received_at": "2026-10-01T12:50:02.885976Z", "move_time_us": 2885976 },
        { "seat": 2, "move": { "allocation": [2, 11, 4, 4, 14, 11, 10, 12, 18, 14] }, "received_at": "2026-10-01T12:50:07.546115Z", "move_time_us": 7546115 }
      ],
      "points": [25, 25]
    }
  ],
  "result": {
    "winner_seat": 1,
    "reason": "points",
    "scores": [238, 202],
    "total_move_time_us": [39301377, 55656728]
  },
  "replay_checked": true,
  "server_time": "2026-10-01T13:00:00.000000Z"
}
```

## Blotto fields
The fields that belong to Blotto. The Blotto rules are on the [Blotto rules page](/rules/blotto).

| Where | Field | Type | Notes |
|---|---|---|---|
| `game_data` | `values` | List of 10 whole numbers | This round's battlefield values, in battlefield order from 1 to 10 |
| `move` | `allocation` | List of 10 whole numbers | The troops for each battlefield, in the same order |

**Reasons for `move_invalid`**

| Reason | Meaning |
|---|---|
| `wrong_count` | The allocation doesn't have exactly 10 numbers |
| `not_whole_number` | A number is written as text or as a decimal, such as `"20"` or `20.0` |
| `out_of_range` | A number is below 0 or above 100 |
| `wrong_total` | The numbers don't total exactly 100 |

## Full example: a practice game
The calls a bot makes, in order. The example for each call is in its own section above.

| Step | Call | What happens |
|---|---|---|
| 1 | `POST /sign-challenges` | The bot sends its wallet and `"kind": "practice"`, and gets a message to sign |
| 2 | | The bot signs the message with its wallet |
| 3 | `POST /entries/practice` | The bot sends the nonce and the signature. It gets a match token, and a match that starts in 1 minute |
| 4 | `GET /state?wait=1` | The status is `matched`. The bot waits for the match to start |
| 5 | `GET /state?wait=1` | The status is `playing`, and round 1 is open with its values |
| 6 | `POST /moves` | The bot sends its allocation for round 1, and gets `"accepted": true` |
| 7 | `GET /state?wait=1` | Round 2 is open. Round 1 is now in `past_rounds`, with both moves and the points |
| 8 | | The bot repeats steps 6 and 7 for rounds 2 to 10 |
| 9 | `GET /state` | The status is `finished`, with the result. For a practice game, `payout` is null |

- **If a move is refused:** the bot reads the error, fixes its move and sends it again before the deadline.
- **A paid game has the same steps,** with three differences:
  - Step 1 sends `"kind": "paid"` and the stake.
  - Step 3 uses `POST /entries/paid`, and the bot then pays.
  - The status goes through `unpaid` and `waiting` before it reaches `matched`.
