Casino Integration — Server-to-Server Authentication
This document is the contract for casino engineers integrating their backend with the prediction-markets platform.
It describes how we authenticate to you: every server-to-server call the prediction-markets platform makes to your casino's API (e.g. the upcoming wallet debit/credit calls) carries a signed token that you verify. This is the outbound direction and is separate from how your players authenticate to us.
For a plain-language walkthrough of the flow (who does what, and why), see private-key-jwt-explained.md. This document is the implementation contract.
Two independent directions — don't confuse them.
Direction Who signs Algorithm Where the key lives Inbound — player → us Your backend HS256, shared hmacSecretSecret you hold, given at onboarding Outbound — us → your backend (this doc) Our backend ES256, our private key You only ever hold our public key The inbound shared secret is never reused for outbound calls.
1. The model: private_key_jwt
On every server-to-server request we send you, we attach a short-lived JWT in
the Authorization header:
Authorization: Bearer <JWT>
The JWT is signed with ES256 (ECDSA on the P-256 curve) using our private key. You verify it with our public key, which we publish as a JSON Web Key Set (JWKS). You never hold a secret of ours — a leak of your database cannot be used to impersonate us, and nothing is shared between casinos.
There is no token-exchange step: the JWT is itself the credential, presented fresh on each call. You do not need to run an OAuth token endpoint.
2. Token format
Header
{
"alg": "ES256",
"kid": "gIeicZJ1yIkQbjShOz7L3N6FTnFKy8ClCn54F__aFQg",
"typ": "JWT"
}
algis alwaysES256. Reject any other value (in particularnoneand any HMACHS*algorithm).kididentifies which key in our JWKS signed the token. Use it to select the verification key.
Claims
| Claim | Example | Meaning / how you validate it |
|---|---|---|
iss |
https://api.prediction-markets.example |
Our identity. Must equal the issuer we give you at onboarding. |
sub |
https://api.prediction-markets.example |
Same as iss — the token asserts our own identity, not a delegated user. |
aud |
casino-acme |
Your assigned audience (see §6). Must equal the value we agreed for you. Reject tokens addressed to anyone else. |
iat |
1750000000 |
Issued-at (epoch seconds). |
exp |
1750000060 |
Expiry (epoch seconds). Tokens live ~60s. Reject if expired (allow small skew, §5). |
jti |
b1c2… (UUID) |
Unique token id. Optional replay defense (§5). |
3. The JWKS endpoint
Fetch our public keys from:
GET https://<our-base-url>/api/.well-known/jwks.json
Example response:
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "X4OXjuCQsXv9svic5dRxtHJmuBKConxegerfg7N7fis",
"y": "jrurAogjqwsVfd2NoMfYOA8PNHSwXrcHQDpDqiVCjVQ",
"kid": "gIeicZJ1yIkQbjShOz7L3N6FTnFKy8ClCn54F__aFQg",
"use": "sig",
"alg": "ES256"
}
]
}
Caching & rotation. The endpoint sends Cache-Control: max-age=300. Cache
the key set and index it by kid. When you receive a token whose kid is not
in your cache, re-fetch the JWKS once and retry — this is how you pick up a key
rotation with no coordination. During a rotation we publish the new key
alongside the retiring one, so tokens signed by either verify throughout the
overlap.
Use a JWKS client that handles caching and the unknown-
kidre-fetch for you (e.g.jwks-rsa/jose'screateRemoteJWKSetin Node,PyJWKClientin Python). Don't fetch the JWKS on every request.
4. Verification algorithm
For every inbound server-to-server request from us:
- Extract the bearer token from the
Authorizationheader. - Decode the header; confirm
alg === "ES256". Reject anything else. - Select the public key from the JWKS by
kid(re-fetch on unknownkid). - Verify the ES256 signature against that key.
- Validate claims:
issequals the issuer we gave you.audequals your assigned audience.expis in the future andiatis not in the future, allowing ±60s skew.
- (Optional) Reject if you have seen
jtibefore within its lifetime.
If any step fails, respond 401 Unauthorized and do not process the request.
Node.js example (jose)
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://<our-base-url>/api/.well-known/jwks.json'),
);
export async function verifyPlatformToken(authorizationHeader: string) {
const token = authorizationHeader.replace(/^Bearer\s+/i, '');
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://api.prediction-markets.example', // the iss we gave you
audience: 'casino-acme', // your assigned aud
algorithms: ['ES256'],
clockTolerance: 60, // seconds
});
return payload; // safe to act on the request
}
Python example (PyJWT)
import jwt
from jwt import PyJWKClient
jwks = PyJWKClient("https://<our-base-url>/api/.well-known/jwks.json")
def verify_platform_token(authorization_header: str) -> dict:
token = authorization_header.removeprefix("Bearer ").strip()
signing_key = jwks.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["ES256"],
issuer="https://api.prediction-markets.example",
audience="casino-acme",
leeway=60,
)
5. Clock skew & replay
- Skew. Allow a tolerance of about ±60 seconds on
exp/iat. Our tokens are short-lived; without skew tolerance, clock drift between our servers and yours can cause spurious rejections. - Replay (optional). Each token has a unique
jti. If you want replay protection beyond the shortexpwindow, cache seenjtis until they expire and reject duplicates. Combined with HTTPS this is usually unnecessary, but it's available.
6. What we exchange at onboarding
| Item | Provided by | Notes |
|---|---|---|
| JWKS URL | Us | https://<our-base-url>/api/.well-known/jwks.json |
Issuer (iss) |
Us | The exact string you must require. |
Audience (aud) |
Agreed | The identifier we stamp on tokens addressed to you. Defaults to your tenant slug; tell us if you need a specific value (e.g. your API's base URL or a client id). |
| Your API base URL | You | Where we send wallet calls (configured in the next phase). |
Static public key fallback
If you cannot fetch and cache a JWKS, we can instead hand you a single pinned
public key (PEM) out of band. Verification is identical except you skip the
JWKS fetch and kid selection. The trade-off: key rotation then requires us to
send you the new key and you to deploy it, rather than it propagating
automatically. JWKS is strongly preferred.
7. Future authentication modes
The platform models an auth mode per casino. Today only PRIVATE_KEY_JWT (this
document) is implemented. Two more are reserved for casinos with specific needs
and slot in behind the same onboarding process:
OAUTH2_CLIENT_ASSERTION— if you run an OAuth2 token endpoint, we can present the very same signed JWT as an RFC 7523 client assertion to obtain an access token. Same keys, same JWKS.HMAC— a shared-secret signature, offered only as a last resort for casinos that cannot verify asymmetric JWTs.
If neither applies, no action is needed: PRIVATE_KEY_JWT is the default.