openapi: 3.1.0
info:
  title: Wallet API
  version: "1.0"
  summary: The three endpoints your wallet implements for your kinematic game server.
  description: |
    In live mode your game server calls your wallet to debit stakes, credit payouts and read balances. You implement these endpoints under the wallet URL you set in the dashboard's Settings; the game server appends `debit`, `credit` or `balance` to it.

    Every call is a `POST` with a JSON body, signed with your `WALLET_SECRET`. Every ID is idempotent: the game server retries a call with the same ID and the same body until it gets a valid receipt. Amounts, balances and revisions are integer strings in the currency's minor units.
  contact:
    name: kinematic
    email: hello@kinematic.run
servers:
  - url: https://wallet.luckystar.example/kinematic
    description: Your wallet URL from Settings. It must use https.
tags:
  - name: Wallet
    description: Endpoints your wallet answers. Your game server is the caller.
paths:
  /debit:
    post:
      tags: [Wallet]
      operationId: debit
      summary: Debit stakes
      description: |
        Takes the stakes of one or more bets from the player's balance in one atomic change.

        Apply the whole `amount` and answer `status: "applied"`, or apply nothing and answer `status: "declined"` when the player cannot cover it. Record the decision durably under the ID: a declined ID stays declined if funds arrive later, and an applied ID is never applied twice. Never debit part of a batch.

        Record which rounds each applied debit named. If your wallet checks credits, it pays only rounds it debited.

        A Blackjack double, split or insurance is a separate debit with an ID that starts with `stake_`: its `rounds` holds the round's bet ID and `amount` is the extra stake.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Timestamp"
        - $ref: "#/components/parameters/Signature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DebitRequest"
            examples:
              batch:
                summary: Two bets debited together
                value:
                  id: batch_3a787ce18225e69054f0bfbe34e909d7
                  player: player_48213
                  currency: USD
                  amount: "7500"
                  kind: debit
                  rounds:
                    - 1eff8f3759107f88d5d63eaf980a9459
                    - d5963bfae4ad9081315e9159548f8541
              stake:
                summary: A Blackjack double
                value:
                  id: stake_e5adbf27e5ff4e5283b9dc871388a32d
                  player: player_48213
                  currency: USD
                  amount: "2500"
                  kind: debit
                  rounds:
                    - 8d10611c88a4f04ae84cc76caa182257
      responses:
        "200":
          description: The debit is applied, or durably declined.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Receipt"
              examples:
                applied:
                  summary: Applied
                  value:
                    id: batch_3a787ce18225e69054f0bfbe34e909d7
                    status: applied
                    balance: "92500"
                    revision: "41"
                    currency: USD
                declined:
                  summary: Declined for funds
                  value:
                    id: batch_3a787ce18225e69054f0bfbe34e909d7
                    status: declined
                    balance: "4200"
                    revision: "40"
                    currency: USD
        default:
          $ref: "#/components/responses/Ambiguous"
  /credit:
    post:
      tags: [Wallet]
      operationId: credit
      summary: Credit a payout or a refund
      description: |
        Adds a round's gross payout, or a refunded stake, to the player's balance.

        A payout is gross: it includes the returned stake, because the debit took the whole stake. A losing round has no credit. Payouts have IDs that start with `payout_`; refunds start with `refund_` and carry `refund` instead of `vrf` and `steps`.

        Never decline a credit for funds: answer `status: "applied"`. A wallet that checks credits refuses one that fails the check with HTTP 409 and the body in `CreditRefusal`; the game server keeps the credit and retries it.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Timestamp"
        - $ref: "#/components/parameters/Signature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreditRequest"
            examples:
              payout:
                summary: A Dice win
                value:
                  id: payout_091aaed882b1fb6a076c877ff78bb307
                  player: player_48213
                  currency: USD
                  amount: "9900"
                  kind: credit
                  rounds:
                    - 1eff8f3759107f88d5d63eaf980a9459
                  settlements:
                    - round: 1eff8f3759107f88d5d63eaf980a9459
                      amount: "9900"
                      bet:
                        type: bet
                        id: 1eff8f3759107f88d5d63eaf980a9459
                        wager:
                          stake: "5000"
                          bet:
                            game: dice
                            target: 5000
                            over: true
                        seedHash: e28489e5d5c71d154a9def37a5369339f566e62267ac3222f28c0fc35ac7c0c8
                        authorization:
                          payload:
                            format: og.proof.v3
                            kind: authorization
                            wallet: '["lucky-star","player_48213","USD"]'
                            math: dbc64afcb07c0d1c029c9cea07118f495f5a6e350a090ddf4ae6f804079ae132
                            command:
                              type: bet
                              id: 1eff8f3759107f88d5d63eaf980a9459
                              wager:
                                stake: "5000"
                                bet:
                                  game: dice
                                  target: 5000
                                  over: true
                              seedHash: e28489e5d5c71d154a9def37a5369339f566e62267ac3222f28c0fc35ac7c0c8
                          publicKey: XfuCuSvxdGBmTC2mYSZ8WLJ0PiuBOD1_k3hs4YK02v0
                          signature: 2_1mtd2ESweqUBgTojF56idR7mGnU-CA6mSSu--xrt9SV9qskbyyM8bQ1cSKfH-6Xwyp9rrR7iG6YYbG5YhRzA
                      vrf:
                        keyId: 62516d82119a9f4eb688da80f3f41548
                        seed: e9ca18e3c557e52e3e6b2feb062e7ab68a8a822114c4de4eabeb0ee1db79214c
                        proof: 20864c99b79c5037ce8be2768b475757253c431f46d8165b73ed189c4831adec
                        output: bafc2f548ebacb0be2767181ac9824cd917b5c3f387c1edaf1ecb4bd8082fa4c
                      steps: []
              refund:
                summary: An abandoned bet's stake
                value:
                  id: refund_4abf140d6ee9b6948e9da913bc3932b7
                  player: player_48213
                  currency: USD
                  amount: "2500"
                  kind: credit
                  rounds:
                    - d5963bfae4ad9081315e9159548f8541
                  settlements:
                    - round: d5963bfae4ad9081315e9159548f8541
                      amount: "2500"
                      bet:
                        type: bet
                        id: d5963bfae4ad9081315e9159548f8541
                        wager:
                          stake: "2500"
                          bet:
                            game: limbo
                            target: 200
                        seedHash: cd7b667a7a5cec7400ae8adea855fc51118094b318aeecd5606781309f0dba1d
                        authorization:
                          payload:
                            format: og.proof.v3
                            kind: authorization
                            wallet: '["lucky-star","player_48213","USD"]'
                            math: dbc64afcb07c0d1c029c9cea07118f495f5a6e350a090ddf4ae6f804079ae132
                            command:
                              type: bet
                              id: d5963bfae4ad9081315e9159548f8541
                              wager:
                                stake: "2500"
                                bet:
                                  game: limbo
                                  target: 200
                              seedHash: cd7b667a7a5cec7400ae8adea855fc51118094b318aeecd5606781309f0dba1d
                          publicKey: XfuCuSvxdGBmTC2mYSZ8WLJ0PiuBOD1_k3hs4YK02v0
                          signature: 8Tw68L2xjvnKWEkqLIDMO3crynANbyfBdZD3zVfkab0Di30dmp3yKMNQqybZUlmfA0-miUuyOlaXw1adSc0Y3A
                      refund: abandoned
      responses:
        "200":
          description: The credit is applied. Credits are never declined.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Receipt"
              example:
                id: payout_091aaed882b1fb6a076c877ff78bb307
                status: applied
                balance: "102400"
                revision: "42"
                currency: USD
        "409":
          description: |
            Only for wallets that check credits: the credit failed the check, and nothing changed. The game server keeps the credit under the same ID and body and retries it, and the round stays owed until your wallet and the game server agree.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditRefusal"
              example:
                error: credit_unverified
                reason: proof
        default:
          $ref: "#/components/responses/Ambiguous"
  /balance:
    post:
      tags: [Wallet]
      operationId: balance
      summary: Read a balance
      description: |
        Returns the player's current balance and your wallet's current revision. It changes nothing and needs no idempotency history.

        The game server reads a player's balance at most once every 5 seconds, plus once after each wallet events hint you send it. Settlements already return the post-operation balance.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Timestamp"
        - $ref: "#/components/parameters/Signature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BalanceRequest"
            example:
              id: balance_ace41cf2254913a32f4cb1d7f45eb476
              player: player_48213
              currency: USD
              kind: balance
              amount: "0"
              rounds: []
      responses:
        "200":
          description: The current balance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Receipt"
              example:
                id: balance_ace41cf2254913a32f4cb1d7f45eb476
                status: applied
                balance: "102400"
                revision: "42"
                currency: USD
        default:
          $ref: "#/components/responses/Ambiguous"
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Equals the body's `id`.
      schema:
        type: string
        examples:
          - batch_3a787ce18225e69054f0bfbe34e909d7
    Timestamp:
      name: X-Kinematic-Timestamp
      in: header
      required: true
      description: |
        When the game server signed this attempt, in Unix milliseconds. Refuse a call whose timestamp is more than 60 seconds from your clock. A retry carries a fresh timestamp.
      schema:
        type: string
        pattern: "^[0-9]{13}$"
        examples:
          - "1793629805120"
    Signature:
      name: X-Kinematic-Signature
      in: header
      required: true
      description: |
        Lowercase hex HMAC-SHA256, keyed with your `WALLET_SECRET`, over the exact UTF-8 bytes of `timestamp + "." + body`. Compare it in constant time before you parse the body, and refuse the call with a status other than 200 when it does not match.
      schema:
        type: string
        pattern: "^[0-9a-f]{64}$"
        examples:
          - 0315897c76ddcf819b7603a809501ff9ef1dd518187448e6d41597b6d4b7a021
  responses:
    Ambiguous:
      description: |
        Any status other than 200, a timeout after 8 seconds, a redirect, a body that is not a JSON object of at most 16 KiB, or a receipt whose `id`, `currency` or amounts do not match: the game server cannot tell whether the call applied. It keeps the job and sends the same ID and body again after 1, 2, 4 and up to 30 seconds. It never treats such an answer as a declined debit.
  schemas:
    PlayerId:
      type: string
      description: Your stable player ID, the `sub` of the session token.
      pattern: "^[A-Za-z0-9_-]{1,128}$"
      examples:
        - player_48213
    Currency:
      type: string
      description: The currency code. Each currency is a separate account of the player.
      pattern: "^[A-Z0-9]{2,12}$"
      examples:
        - USD
    MinorUnits:
      type: string
      description: A canonical non-negative integer string in the currency's minor units. No decimals, signs or leading zeros.
      pattern: "^(0|[1-9][0-9]{0,18})$"
      examples:
        - "9900"
    BetId:
      type: string
      description: A bet ID, chosen by the player's browser. The element uses 32 lowercase hex characters.
      pattern: "^[A-Za-z0-9_-]{16,80}$"
      examples:
        - 1eff8f3759107f88d5d63eaf980a9459
    DebitRequest:
      type: object
      required: [id, player, currency, amount, kind, rounds]
      properties:
        id:
          type: string
          description: The idempotency ID. `batch_` for bets, `stake_` for a Blackjack double, split or insurance.
          pattern: "^(batch|stake)_[0-9a-f]{32}$"
        player:
          $ref: "#/components/schemas/PlayerId"
        currency:
          $ref: "#/components/schemas/Currency"
        amount:
          $ref: "#/components/schemas/MinorUnits"
        kind:
          type: string
          const: debit
        rounds:
          type: array
          description: The bets this debit pays for. Their stakes add up to `amount`. A batch holds up to 1,000 bets.
          minItems: 1
          items:
            $ref: "#/components/schemas/BetId"
    CreditRequest:
      type: object
      required: [id, player, currency, amount, kind, rounds, settlements]
      properties:
        id:
          type: string
          description: The idempotency ID. `payout_` for a payout, `refund_` for a refund.
          pattern: "^(payout|refund)_[0-9a-f]{32}$"
        player:
          $ref: "#/components/schemas/PlayerId"
        currency:
          $ref: "#/components/schemas/Currency"
        amount:
          $ref: "#/components/schemas/MinorUnits"
        kind:
          type: string
          const: credit
        rounds:
          type: array
          description: The rounds this credit settles. Each credit pays one round today.
          minItems: 1
          items:
            $ref: "#/components/schemas/BetId"
        settlements:
          type: array
          description: One settlement per round of `rounds`, in the same order. Their amounts add up to `amount`.
          minItems: 1
          items:
            $ref: "#/components/schemas/Settlement"
    BalanceRequest:
      type: object
      required: [id, player, currency, kind, amount, rounds]
      properties:
        id:
          type: string
          pattern: "^balance_[0-9a-f]{32}$"
        player:
          $ref: "#/components/schemas/PlayerId"
        currency:
          $ref: "#/components/schemas/Currency"
        kind:
          type: string
          const: balance
        amount:
          type: string
          const: "0"
        rounds:
          type: array
          maxItems: 0
          items:
            $ref: "#/components/schemas/BetId"
    Receipt:
      type: object
      required: [id, status, balance, revision, currency]
      properties:
        id:
          type: string
          description: The request's `id`.
        status:
          type: string
          enum: [applied, declined]
          description: "`declined` only for a debit the player cannot cover. Credits and balance reads are always `applied`."
        balance:
          $ref: "#/components/schemas/MinorUnits"
          description: The player's balance after this operation, in minor units.
        revision:
          $ref: "#/components/schemas/MinorUnits"
          description: |
            Your wallet-wide revision after this operation. It increases with every change to the account, including changes outside kinematic. Two different balances never share a revision.
        currency:
          $ref: "#/components/schemas/Currency"
          description: The request's `currency`.
    Settlement:
      type: object
      required: [round, amount, bet]
      properties:
        round:
          $ref: "#/components/schemas/BetId"
        amount:
          $ref: "#/components/schemas/MinorUnits"
          description: What the round pays, gross.
        bet:
          $ref: "#/components/schemas/SignedBet"
        vrf:
          $ref: "#/components/schemas/VrfProof"
        steps:
          type: array
          description: A payout's round steps (the player's actions and Crash's server ticks). Empty for instant games.
          items:
            $ref: "#/components/schemas/Step"
        manifest:
          type: object
          additionalProperties: true
          description: A payout of a bet at a return to player other than 99%, the payload of the signed math manifest the bet was placed under. Its SHA-256 is the `math` the player's authorization signs, and `catalog.formula_edge_bps` is the round's house edge. Absent at 99%.
        refund:
          type: string
          enum: [abandoned, engine_failure, key_retired, stake]
          description: |
            A refund instead of a payout. `abandoned`: the bet was accepted and its seed never arrived within 24 hours. `engine_failure`: the engine could not evaluate the revealed round. `key_retired`: the debit applied only after the VRF key the bet's manifest names had retired, so the bet was never accepted. `stake`: a staked action the engine could not apply.
        action:
          $ref: "#/components/schemas/SignedAction"
    SignedBet:
      type: object
      description: The player's signed bet command, exactly as the game server accepted it.
      required: [type, id, wager, seedHash, authorization]
      properties:
        type:
          type: string
          const: bet
        id:
          $ref: "#/components/schemas/BetId"
        wager:
          $ref: "#/components/schemas/Wager"
        seedHash:
          type: string
          description: Lowercase hex SHA-256 of the domain `og.seedhash.v1` and the bet's 32-byte seed.
          pattern: "^[0-9a-f]{64}$"
        authorization:
          $ref: "#/components/schemas/Authorization"
    SignedAction:
      type: object
      description: The player's signed action whose `stake` a `stake` refund returns.
      required: [type, id, roundId, action, authorization]
      properties:
        type:
          type: string
          const: action
        id:
          type: string
        roundId:
          $ref: "#/components/schemas/BetId"
        action:
          type: object
          description: The action, for example `{"kind":"double","hand":0,"stake":"2500"}`.
          additionalProperties: true
        steps:
          type: integer
        authorization:
          $ref: "#/components/schemas/Authorization"
    Wager:
      type: object
      required: [stake, bet]
      properties:
        stake:
          $ref: "#/components/schemas/MinorUnits"
        bet:
          type: object
          description: The game and its settings, for example `{"game":"dice","target":5000,"over":true}`.
          required: [game]
          additionalProperties: true
          properties:
            game:
              type: string
              enum: [mines, plinko, dice, limbo, crash, roulette, keno, baccarat, wheel, pump, diamonds, hilo, blackjack]
    Authorization:
      type: object
      description: The player's Ed25519 signature over the command, under the device key the session allowed.
      required: [payload, publicKey, signature]
      properties:
        payload:
          type: object
          description: |
            `{format: "og.proof.v3", kind: "authorization", wallet, math, command}`. `wallet` is the JSON text of `[casino ID, player, currency]`; `math` is the SHA-256 of the signed game math manifest.
          additionalProperties: true
        publicKey:
          type: string
          description: The player's raw Ed25519 public key, unpadded base64url (43 characters).
        signature:
          type: string
          description: The Ed25519 signature, unpadded base64url (86 characters).
    VrfProof:
      type: object
      description: A payout's proof under the VRF key the acceptance named.
      required: [keyId, seed, proof, output]
      properties:
        keyId:
          type: string
          description: The VRF key's ID, 32 lowercase hex characters.
          pattern: "^[0-9a-f]{32}$"
        seed:
          type: string
          description: The seed the player's browser revealed, 64 lowercase hex characters.
          pattern: "^[0-9a-f]{64}$"
        proof:
          type: string
          description: The VRF proof, a compressed BN254 G1 point of 32 bytes as 64 lowercase hex characters.
          pattern: "^[0-9a-f]{64}$"
        output:
          type: string
          description: SHA-256 of `og.vrf.v1/out` and the proof, 64 lowercase hex characters.
          pattern: "^[0-9a-f]{64}$"
    Step:
      type: object
      required: [action, elapsed]
      properties:
        action:
          type: object
          description: The player's action, such as `{"kind":"reveal","index":7}`, or Crash's `{"kind":"tick"}`.
          additionalProperties: true
        elapsed:
          type: integer
          description: Milliseconds since the round started, on the game server's clock.
    CreditRefusal:
      type: object
      required: [error, reason]
      properties:
        error:
          type: string
          const: credit_unverified
        reason:
          type: string
          description: The first check that failed.
          enum: [malformed, amount, authorization, wallet, no_debit, duplicate, refund, key, seed, proof, manifest, engine, outcome]
