velven
Docs
Menu

Leaderboards

Velven hosts your space's leaderboards and hands the rows back. It draws no board of its own: your space draws it, in its own style, from the rows it reads.

View guide as Markdown

Declare a board

Declare boards in the page's <head>, in a <script type="application/velven+json"> block beside the proof tag. A score posted to a key you never declared is refused with no_board. Up to 10 boards per space.

index.html
<meta name="velven" content="@handle"><script type="application/velven+json">{"boards":[{"key":"main","trust":"server","metric":"time","sort":"asc","cooldown":10}]}</script>

Every field except key has a default, so the smallest block is {"boards":[{"key":"main"}]}.

Velven reads the block when the space is listed, on its background check every few hours, and when you press Check now or Sync now on the space's edit page.

Note: Remove a board from the block and it is withdrawn: hidden from every read, with its scores kept. Name it again and it comes back.

Board fields

key1 to 32 lowercase letters, digits, - or _
The name calls use for the board. A call that names no board uses main.
trustserver, clientDefault server
Who may post. server: only your own server, with the secret and the player's token. client: the page, under the board's range, cooldown and caps. Server scores helps you choose.
metricpoints, time, distance, level, customDefault points
What the value measures. Use custom with a label and unit for anything else.
labelUp to 40 characters
The board's name, for you to draw. Optional.
unitUp to 16 characters
Drawn after the value. Optional.
sortdesc, ascDefault desc
desc: higher is better. asc: lower is better, as for a time.
modebest, sumDefault best
best: a player's best submission ranks. sum: their submissions add up.
entriesplayer, runDefault player
player: one row per player. run: one row per submission, like an arcade table, so one player can hold several ranks. run needs mode best.
minNumber, decimals allowed
The lowest value accepted. Anything lower is out_of_range.
maxNumber, decimals allowed
The highest value accepted. Anything higher is out_of_range.
cooldown0 to 86400 secondsDefault 0
The least time between two submissions from one player.
periodall, daily, weeklyDefault all
daily and weekly boards roll over at 00:00 UTC, weeks starting on Monday. Past days and weeks stay readable.
seasonUp to 32 characters
A name for a fresh start. Changing metric needs a new season. Past seasons stay readable.

Change sort, mode and entries at any time. Velven keeps every submission and ranks from them, so a rule change never loses a score. Velven never clears a board: period rolls it over for you, and season is your own reset.

Change boards without a deploy

Send the same shape to the REST API with the creator's token. Only the boards you name are written, and the page's next sync overwrites a key the page also names.

Terminal
curl -s -X PUT https://velven.ai/api/spaces/<slug>/boards \  -H "authorization: Bearer $VELVEN_TOKEN" \  -H "content-type: application/json" \  -d '{"boards":[{"key":"main","trust":"server","metric":"time","sort":"asc","cooldown":10}]}'

The space's edit page lists each board and where it came from, the page or the API. It does not edit them.

Post a score

Post a score when a run ends. Inside Velven, the Velven page posts it under the player's own session, so nothing your space sends is a credential.

JavaScript
const result = await Velven.scores.submit(1240, { board: "main", meta: { car: "red" }, requestId: runId });if (result.ok) {  result.rank;     // the player's rank now  result.value;    // this submission  result.total;    // what the board ranks them by: their best, or their sum  result.improved; // whether this submission improved it} else if (result.error === "signed_out") {  showSignInButton(); // then send again}
boardBoard keyDefault "main"
The board to post to.
metaObject, up to 1 KB as JSON
Anything to keep with the score, such as the car or the level. Returned untouched with every read.
requestId1 to 64 letters, digits, - or _
A retry key for this run. See Retry safely.

A value that is not a number, or a meta that is not an object, is a programming mistake and throws a TypeError.

Note: The page cannot post to a server board: submit answers server_only. Your server posts instead; see Server scores.

Read the board

JavaScript
const top = await Velven.scores.top({ board: "main", limit: 10 });// { ok: true, board, trust: "server" | "client", info, rows }// row:  { rank, value, meta, setAt, user: { id, handle, avatar } }// info: { label, unit, metric, sort, mode, entries, period, bucket, season }//        bucket: the UTC date the day or week starts on, null on an all-time boardconst next = await Velven.scores.top({ board: "main", limit: 10, after: top.rows.at(-1) });const near = await Velven.scores.around({ board: "main", each: 5 });const mine = await Velven.scores.mine({ board: "main" });// { ok: true, board, trust, info, row }, row null before their first score
toplimit 1 to 100Default 10
Rows from the top. Page on by passing the last row back as after. Ties go to the earlier submission.
aroundeach 0 to 50Default 5
Rows either side of the player. Needs a signed-in player. On a run board it centres on their best run.
mine
The player's own row, or null before their first score. Needs a signed-in player.
  • Show a personal best from mine, not from a copy in your own storage: that copy drifts from the board when a run is refused or played signed out.
  • Draw the board's label and unit from info rather than repeating them in your page. info is null only from an older Velven page.
  • Every read carries trust. Show it: a client board is only as honest as a browser can be.

Warning: Rows and meta are other players' data, and any signed-in account can put up to 1 KB of JSON in meta. Draw them as text (textContent, never innerHTML) and never merge a row into your own state, or one player's entry can run in every other player's browser.

Past days and seasons

Every read takes bucket and season, so past days, weeks and seasons stay readable.

JavaScript
const yesterday = await Velven.scores.top({ board: "daily", bucket: "previous" });const thatWeek = await Velven.scores.top({ board: "weekly", bucket: "2026-09-23" }); // the week from Monday 2026-09-21const lastSeason = await Velven.scores.mine({ season: "s1" });
bucketcurrent, previous, or a YYYY-MM-DD dateDefault current
A date reads the day or week it falls in.
seasonA season nameDefault The board's current season
Any season the board has had.

Note: Paging with after stays in the bucket and season the row was read from, so a board paged across midnight stays on its day. Pass the row object the read returned: a copy (a spread, a JSON round trip, a framework's store) is not recognised and pages the current bucket, unless you also pass bucket and season from the page's info.

Retry safely

Give each run a requestId. After a failed, such as a lost connection or the 30-second wait running out, send the same run with the same id: it is entered once, and the repeat answers as the first did. Without an id, every call is a new submission.

Moderate a board

Delete a submission or ban a player from the space's edit page, or through the REST API. Deleting a submission re-ranks the player from what is left.

Error codes

A failed score call answers one of these codes in error:

CodeMeaning
signed_outNo player is signed in, or they chose to stay a guest. Offer signIn() from a button, then send again.
unavailableNot inside Velven, or the space is not published with a verified owner. Nothing to fix in the space.
no_boardNo board has that key. Declare it in the page's block or through the REST API.
server_onlyThe board's trust is server, so only your own server may post to it, with the secret.
out_of_rangeThe value is below the board's min or above its max.
cooldownToo soon after this player's last submission. retryAfter is the wait in seconds.
bannedYou banned this player from the space's boards.
invalid_valuemeta is over 1 KB. Answered before anything is sent.
rate_limitedToo many calls in a minute. Wait; do not retry in a loop.
failedVelven could not answer just now. Try once more later.