← Documentation

Highscores and leaderboards

Complete definition, query, submission and result types. The exact source reference includes all browser and server exports; verify the published package version before using a new method.

Create boards under Manage games. Each board has one entry per signed-in player. Enabled boards appear on the game page, with an optional public title falling back to the API name. Guests can read public scores; saving a score requires an account.

Browser SDK

import { leaderboards } from '@silicon-jungle/inkwell-sdk/leaderboards'
const board = leaderboards.board('highscores')
const existing = await leaderboards.find('highscores') // null if absent
const daily = await leaderboards.findOrCreate({ name: 'daily:2026-09-04', sort: 'descending', display: 'numeric' })
const count = await board.getEntryCount()
const result = await board.submit({ score: 12500, method: 'keepBest', details: [3, 42] })
// { score, updated, scoreChanged, previousRank, rank }
const top = await board.list({ scope: 'global', start: 1, limit: 20 })
const friends = await board.list({ scope: 'friends' })
const nearby = await board.aroundMe({ before: 5, after: 5 })
const selected = await board.list({ scope: 'users', usernames: ['jungle'] })
const mine = await board.getMyEntry()
// Queries: { board, total, nextStart, entries: [{ username, avatarUrl, rank, score, details, updatedAt }] }
// Pass nextStart as start for the next global/friends page; null means the end.

The frame bridge supplies the current game and player; never put a creator API key or runtime token into browser code. Friends-only boards reject global, nearby ranges, and explicit-user client queries. Use the friends view or getMyEntry() instead.

Find-or-create preserves an existing board's settings. Browser creation accepts only name, sort and display, requires sign-in, and cannot re-enable disabled boards or change creator policies. It is limited to 20 new-board requests per player/game per UTC day; finding an existing board does not consume this allowance. Backend creation supports the full definition and the 10,000-board game limit.

Game backend

// Inside your defineBackend handler:
await context.leaderboards.define({
  name: 'fastest', communityName: 'Fastest runs',
  sort: 'ascending', display: 'milliseconds', serverWritesOnly: true,
}) // Find-or-create does not change an existing board.
const board = context.leaderboards.board('fastest')
if (!connection.identity.isGuest) {
  await board.submitFor(connection.identity.username, { score: elapsedMs })
}
await board.queryFor('jungle', { scope: 'friends' })

Backend boards expose update(definition), deleteEntry(username), reset(), and delete(). These only affect the credential’s game. Creators decide which results to award; backend-only writes do not certify legitimate gameplay.

Typed read and submit helpers

import { leaderboards, GameServiceError } from '@silicon-jungle/inkwell-sdk'

export async function readHighscores() {
  const board = await leaderboards.find('highscores')
  if (!board) return { entries: [], mine: null }
  const top = await board.list({ scope: 'global', start: 1, limit: 20 })
  const mine = await board.getMyEntry()
  return { entries: top.entries, nextStart: top.nextStart, mine }
}
export async function submitScore(score: number) {
  try {
    const result = await leaderboards.board('highscores').submit({
      score, method: 'keepBest', details: [],
    })
    console.log('Stored score and current rank:', result.score, result.rank)
    return result
  } catch (error) {
    if (error instanceof GameServiceError) console.warn(error.status, error.code)
    // Let the UI distinguish a policy error from an uncertain network outcome.
    throw error
  }
}

Definitions and result contracts

A definition contains name, optional communityName, sort (ascending/descending), display (numeric/seconds/milliseconds), serverWritesOnly, friendsReadsOnly and enabled. Ascending puts smaller scores first; descending puts larger scores first. Display changes presentation, not storage: even seconds/milliseconds scores are integers.

board(name) constructs a client handle without checking existence. find returns a handle or null for a missing board; other failures still reject. get returns board metadata including timestamps. list(offset) on the module enumerates board definitions with nextOffset; list(query) on a board returns board metadata, total, entries and nextStart. Do not mix catalogue offsets with ranked positions.

A submission returns the stored score, updated, scoreChanged, previousRank (null for a first entry) and rank. It does not return a global immutable rank: concurrent submissions can move positions immediately. Ranked pages are current queries, not a snapshot across pagination. An empty entries array can mean no score in that scope, not a failed request.

Errors, retries and backend authority

The browser may only submit for its current player/game. Server credentials may use submitFor or queryFor within their own game. Enabling serverWritesOnly prevents direct browser submissions; validate the result in creator code before awarding it. Client-provided scores alone cannot establish fair play.

Malformed names, methods or non-int32 values can throw TypeError locally. GameServiceError carries status/code for missing boards, sign-in requirements, policy denial and quota limits. These calls are online-only; they are not part of the offline stat queue.

A timed-out submit may already have committed. keepBest protects against replacing a better score but repeated submissions still consume quota; forceUpdate can overwrite a newer result if blindly retried. There is no score-submission requestId receipt API in this reference. Refresh getMyEntry before deciding what to do after an uncertain response.

Reset removes every entry on one board; delete removes the board; deleteEntry removes the chosen player's entry. Use creator/backend authority and explicit confirmation in administrative interfaces. Seasonal/daily boards are names and reset schedules your game manages, not a built-in season scheduler.

HTTP API

POST JSON to /api/v1/game-services/leaderboards with a runtime bearer token, or /api/v1/games/:slug/leaderboards with an approved creator API key.

{ operation: 'define', name: 'highscores', definition: {
  communityName: 'High scores', sort: 'descending', display: 'numeric',
  serverWritesOnly: false, friendsReadsOnly: false, enabled: true
} }
{ operation: 'query', name: 'highscores', username: 'jungle', query: { scope: 'friends' } }
{ operation: 'submit', name: 'highscores', username: 'jungle', score: 100, method: 'keepBest', details: [] }
// Also: list (+ offset), get, count, findOrCreate (+ definition), update (+ complete definition), reset,
// delete, deleteEntry (+ username).

Limits

  • Signed int32 scores and up to 64 int32 details; no replay attachments.
  • 10,000 boards per game, paginated 100 at a time.
  • One-based global/friends start and at most 100 entries. Up to 100 explicit usernames; absent entries are omitted.
  • Around-player ranges accept any before/after combination totalling at most 99, plus the player's own entry. Players without a score get an empty list.
  • 10 submissions per player/game per fixed ten-minute window, across boards and credentials, plus API request limits.
  • Keep-best preserves details on equal/worse scores; force-update replaces both. Equal scores use a stable internal player-ID tie-break.
  • Private names, emails and account IDs are never returned.