velven
Docs
Menu

Server scores

A server board takes scores only from your own server, with the space's secret and the player's token. It is as honest as the checks your server runs.

View guide as Markdown

Choose a tier

TierWho postsWhat it proves
clientThe page, through the Velven pageThe player's word, within the board's range, cooldown and caps.
serverYour server, with the secret and the player's tokenThat the score came through your server.

A server board is only worth more when your server checks something: it runs the game, or checks the run against its own record. A server that relays the number the page sent proves no more than client, and costs you a function. Choose server when your server has something to check, and client otherwise.

The server can live anywhere. Velven checks the secret and the token, not where the post came from. A space on a static host, such as a ChatGPT site or GitHub Pages, can post through a small function on Vercel, Netlify, Cloudflare or Replit. Since the page calls it from another origin with a JSON body, the browser sends a preflight first: answer OPTIONS with Access-Control-Allow-Origin set to your page's origin and Access-Control-Allow-Headers: content-type, and send the same Access-Control-Allow-Origin on the POST's answer. With no server at all, declare the board client.

Issue the secret

Issue the secret on the space's edit page, or through the REST API. It is shown once and only its hash is kept; issuing again replaces it.

Terminal
curl -s -X POST https://velven.ai/api/spaces/<slug>/secret -H "authorization: Bearer $VELVEN_TOKEN"# { "secret": "…", "message": "Shown once. Put it in the host's server environment …" }

Warning: Put the secret in your server's environment, never in the page.

Send the run from the page

When a run ends, get the player's token with signIn() and send it to your server with the run. Use a plain signIn(), not a silent one: the run's end is the player's own action, so a guest may be offered the card.

game.js
const auth = await Velven.signIn();if (auth.ok && Velven.environment === "local") {  await Velven.scores.submit(score); // the in-memory board, so the page is built and drawn locally} else if (auth.ok) {  await fetch("https://your-function.example.com/api/score", {    method: "POST",    headers: { "content-type": "application/json" },    body: JSON.stringify({ token: auth.token, value: score, request_id: crypto.randomUUID() }),  });}

On your laptop there is no function and the token is fake, so in the local environment the example posts to the in-memory board instead. The SDK takes it with a console note; add ?velven_strict=1 to refuse it as Velven does.

Verify the token and post

On your server, verify the token against https://velven.ai/.well-known/jwks.json: the issuer is https://velven.ai, the audience is your space's origin, the algorithm is ES256, and the space claim is your space's id. Then POST to https://velven.ai/api/v1/scores with the secret in x-velven-secret.

@velven/sdk/server does both with no dependencies, on Node 20 and later, Deno, Bun and Cloudflare Workers. It accepts only Velven's own tokens and has no local mode. Or use any JWT library, such as jose:

import { postScore, verifyToken } from "@velven/sdk/server";export default async (request) => {  const { token, value, request_id } = await request.json();  // Your listing's id (Velven.space.id in the page), checked when the variable is set.  const space = process.env.VELVEN_SPACE_ID ? Number(process.env.VELVEN_SPACE_ID) : undefined;  const player = await verifyToken(token, { audience: "https://your-space.netlify.app", space });  if (!player.ok) return Response.json({ error: player.error }, { status: 401 });  // Check the run here: this is what makes a server board worth more than a client one.  const result = await postScore({ secret: process.env.VELVEN_BOARD_SECRET, token, value, requestId: request_id });  return Response.json(result); // { ok: true, rank, ... } or { ok: false, error }, for the page to read};

Velven checks the token again itself, so a leaked secret alone cannot post as a player.

Request body

POST /api/v1/scores with the header x-velven-secret and a JSON body:

tokenString
The player's token, from signIn() in the page.
boardBoard key
The board to post to.
valueNumber
The score.
request_idUp to 64 characters
Makes the post idempotent: a retry with the same id answers as the first did and is entered once.
metaObject, up to 1 KB as JSON
Optional. Returned with every read.

Responses

StatusBodyMeaning
200{ "ok": true, "board", "value", "total", "improved", "rank" }Entered. total is what the board ranks the player by now.
400{ "error": "bad_request" }The body was not { token, board, value, request_id } with an optional meta.
401{ "error": "invalid_secret" }No space has that secret. Issue a new one and put it in the environment.
401expired_token or invalid_tokenThe token is past its hour, or Velven did not sign it. Ask the page for a fresh one.
403wrong_spaceThe token was made for another space.
403banned, server_only or client_onlyThe player is banned, or the board's trust does not take this path.
404unavailable or no_boardThe space is not published with a verified owner, or no board has that key.
422{ "error": "out_of_range", "min", "max" } or invalid_valueOutside the board's range, not a finite number, or meta over 1 KB.
429{ "error": "cooldown", "retry_after" } or rate_limitedToo soon for this player, or more than 600 posts from this space in a minute.
503{ "error": "failed" }Velven could not store it just now. Retry with the same request_id.