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

> ## Agent Instructions
> These are the customer docs for kinematic, a name written in lowercase except at the start of a sentence. Kinematic provides provably fair casino games, served to a casino's site by a game server: a Cloudflare Worker that the kinematic dashboard deploys into the casino's own Cloudflare account.
> Never put SESSION_SECRET or WALLET_SECRET in code, prompts, logs, commits or chat. Read them from environment variables and ask the person you work with to set them. The dashboard's Integration page shows each secret once.
> Values such as lucky-star, luckystar.example, og-rgs-lucky-star.luckystar.workers.dev, games.luckystar.example and player_48213 are examples. Use the casino's own casino ID, game server origin, element tag, session issuer and currencies: the Instructions for your coding agent on the dashboard's Integration page hold them, or ask the person you work with.
> To debug, start at https://docs.kinematic.run/troubleshooting and https://docs.kinematic.run/reference/errors. The wallet contract is at https://docs.kinematic.run/api/wallet.openapi.yaml.
> Sandbox bets are recorded on Solana devnet. Live game servers record nothing on Solana until the audited mainnet program is deployed.

# Session tokens

> Sign the short-lived token that tells your game server which player is playing in which currency, and attest the player's device key so they can play on any device.

The game server has no login of its own. Your backend vouches for each player with a session token: an HS256 JWT signed with your `SESSION_SECRET`. The element asks your backend for one when it connects and again before the token expires. The browser never signs or changes a token.

## The session endpoint

Add `POST /api/game-session` to your backend, behind your own player login. It reads the player your login authenticated, signs a token and answers it as plain text with `cache-control: no-store`. To anyone who is not signed in, it answers HTTP 401. The path is your choice; the dashboard's code and these docs use `/api/game-session`.

The element posts a JSON body. Its first call is `{}`; later calls carry the player's device key as `{"publicKey": "<43-character base64url key>"}`.

```js session-token.mjs theme={null}
import { createHmac } from "node:crypto";

const SESSION_SECRET = process.env.SESSION_SECRET;
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");

export function sessionToken({ player, currency, publicKey }) {
  const claims = {
    sub: player, // your stable player ID: 1 to 128 letters, digits, - or _
    tenant: "lucky-star", // your casino ID
    iss: "https://www.luckystar.example", // your session issuer, exactly as in Settings
    aud: "og-rgs-v1",
    currency, // one of your currencies, such as "USD"
    exp: Math.floor(Date.now() / 1000) + 900, // at most one hour ahead
  };
  // The player's device key: the session may sign bets only with this key.
  if (publicKey) claims.cnf = { jwk: { kty: "OKP", crv: "Ed25519", x: publicKey } };
  const unsigned = encode({ alg: "HS256", typ: "JWT" }) + "." + encode(claims);
  return unsigned + "." + createHmac("sha256", SESSION_SECRET).update(unsigned).digest("base64url");
}
```

```js game-session.mjs theme={null}
import { sessionToken } from "./session-token.mjs";

// `player` is the player your own login authenticated for this request.
export async function gameSession(request, player) {
  const { publicKey } = await request.json(); // absent on the element's first call
  const token = sessionToken({ player: player.id, currency: "USD", publicKey });
  return new Response(token, {
    headers: { "content-type": "text/plain", "cache-control": "no-store" },
  });
}
```

Any JWT library works if it produces HS256 with the claims below.

## On the Integration page

The **Sign session tokens** step on **Integration** shows the claims your tokens carry, with your casino ID, your session issuer and your currencies filled in, under **Session token claims**. If your session issuer is not set, the step shows `Set your session issuer in Settings. Your tokens carry it as iss.` with **Open settings**: set it under **Players and sessions**.

The page itself shows no signer and no endpoint. The **Instructions for your coding agent** at the top of the page include a Node signer with your casino ID and issuer filled in. See [Use an AI agent](https://docs.kinematic.run/agents.md).

## Claims

| Claim | Value |
| - | - |
| `sub` | Your stable ID for the player: 1 to 128 ASCII letters, digits, `_` or `-`. The same player must always get the same `sub`, because the game server keeps one balance view and one bet history per `sub` and currency. |
| `tenant` | Your casino ID, for example `lucky-star`. |
| `iss` | Your session issuer, exactly as set in **Settings**. |
| `aud` | Always `og-rgs-v1`. |
| `currency` | One of the currency codes your casino serves, for example `USD`. You may leave it out while your casino serves exactly one currency. |
| `exp` | Expiry as an integer number of seconds since the epoch, in the future and at most 3600 seconds ahead. |
| `cnf` | Optional. The player's device key, `{"jwk": {"kty": "OKP", "crv": "Ed25519", "x": "<publicKey>"}}`. |

The game server answers `unauthorized` to a token that fails any of these rules, with no detail about which one. [Session token reference](https://docs.kinematic.run/reference/session-token.md) lists every check in order.

## Currencies

A token names one currency. A player who plays in two currencies has two balances and needs a token for each: sign the token for the currency the page shows. The game server's catalog and limits follow the token's currency.

## Device keys

The element signs every bet and action with an Ed25519 key that it keeps in the player's browser. That signature lets the player prove later that they placed the bet.

* **With `cnf`.** When your token carries the device key the element sent you, the session may sign only with that key, and the game server enrolls the key for the player the first time it signs. A player can then continue on a second device, after clearing site data or in a private window, and can cash out a round opened elsewhere.
* **Without `cnf`.** The first key that signs for a player enrolls on first use. Any other key later fails with `player_key_changed`, and the player stays locked out until you attest a new key with `cnf`.

Put `cnf` on every token you sign. Attest only a key that the player you authenticated for this request sent you, and pass it through unchanged: the game server refuses any other `cnf` shape as `unauthorized`.

Enrollment only adds keys. It never changes an earlier proof: each bet keeps the key that signed it.

## Lifetime and renewal

* Sign tokens for a few minutes; the examples use 15. The element asks for a new token shortly before the current one expires, and moves its connection to the new token without a disconnect.
* The element asks for one token at a time and reuses it until renewal. A token request that takes longer than 10 seconds fails and is retried with backoff.
* If your endpoint answers the same token again, the element asks again later. Answer a fresh token each time.
* Never cache tokens in the browser yourself, and never sign them in the browser.

> [!NOTE]
>
> The element's first call carries no device key. The element reads the player and currency from that first token, then asks again with its device key before it opens its connection. A browser that cannot keep a device key uses the first token.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.