Official SDK

lightleaderboard npm package

TypeScript-first SDK for Node.js 18+, browsers, Bun, Deno, and Cloudflare Workers. Zero dependencies — uses native fetch and the Web Crypto API. Keep your apiKey server-side; the SDK is safe to bundle for Node.js backends or server functions.

1

Install

npm install lightleaderboard
# or
yarn add lightleaderboard
# or
pnpm add lightleaderboard

Requires Node.js 18+ or any runtime with native fetch + Web Crypto API (Bun, Deno, Cloudflare Workers, modern browsers).

2

Initialize the client

Create a single instance per game and reuse it across your server. Find your API key and game ID in the developer portal → your game → API Keys.

import { LightLeaderboard } from 'lightleaderboard';

const lb = new LightLeaderboard({
  apiKey: process.env.LLB_API_KEY,  // from the LightLeaderboard dashboard
  gameId: 'my_game',                // your game's reference ID
});
Never expose apiKey to browser clients. For browser-based games (Kaboom.js, Phaser, PixiJS), call a server endpoint that uses the SDK — see the Express example below.
3

Submit a score

Submit a score and get back the player's rank, personal-best flag, and total player count in a single call — no extra request needed.

const result = await lb.submitScore({
  score:        9500,
  playerRefId:  'player-123',        // your internal player ID
  playerName:   'Alice',             // display name
  submissionId: crypto.randomUUID(), // idempotency key — prevents duplicate submissions
  playTimeMs:   120_000,             // optional: run duration in ms
  seasonId:     'season-3',          // optional: season bucket
  teamId:       'team-red',          // optional: team bucket
  metadata:     { level: 5, combo: 12 }, // optional: arbitrary JSON
});

// Result comes back in the same call — no second request needed
console.log(result.rank);            // 4       ← player's current rank
console.log(result.isPersonalBest);  // true
console.log(result.totalPlayers);    // 1024
console.log(result.deduped);         // true if submissionId was already seen
ParameterTypeRequiredDescription
scorenumberThe player's score
playerRefIdstringYour internal player ID — enables deduplication and rank tracking
playerNamestringDisplay name shown on the leaderboard
submissionIdstringIdempotency key — resubmitting the same ID returns the original result without creating a duplicate
seasonIdstringBucket this score into a season (e.g. "season-3")
teamIdstringBucket this score into a team
playTimeMsnumberRun duration in milliseconds
metadataobjectAny extra JSON — stored and returned alongside the score
4

Fetch the leaderboard

Returns one entry per player (their best score) by default. Filter by period, season, or team. Use offset for pagination.

const { entries } = await lb.getLeaderboard({
  limit:  10,          // 1–100, default 20
  offset: 0,           // for pagination
  period: 'weekly',    // 'all' | 'weekly' | 'monthly'
  season: 'season-3',  // optional
  team:   'team-red',  // optional
});

entries.forEach(e => {
  console.log(`#${e.rank}  ${e.playerName}  ${e.score}`);
});

// Pagination — load the next page
const page2 = await lb.getLeaderboard({ limit: 10, offset: 10 });
5

Get a player's rank

Returns the player's rank, score, total player count, and percentile. percentile is 0–100 — higher means better. Rank 1 of 100 = percentile 100.

const data = await lb.getPlayerRank('player-123', {
  period: 'weekly', // optional
});

console.log(data.rank);         // 4
console.log(data.score);        // 9500
console.log(data.totalPlayers); // 1024
console.log(data.percentile);   // 99.7 — top 0.3% of players
// percentile is 0–100, higher = better. rank 1 of 100 → percentile 100
6

More methods

getCentricLeaderboard

Fetch the leaderboard centered on a specific player — the entries immediately above and below them. Great for in-game "you vs. your neighbours" screens.

// Shows the players immediately above and below the given player.
// Perfect for in-game "you vs. your neighbours" screens.
const { entries, playerRank } = await lb.getCentricLeaderboard('player-123', {
  limit: 11, // 5 above + player row + 5 below
});
getPlayer / updatePlayer

Fetch or upsert a player profile. Fields are merged — omitted fields keep their current value.

// Fetch a player profile
const profile = await lb.getPlayer('player-123');
console.log(profile.playerName, profile.avatarUrl, profile.country);

// Create or update a profile — omitted fields keep their current value
await lb.updatePlayer('player-123', {
  playerName: 'Alice',
  avatarUrl:  'https://example.com/avatar.png',
  country:    'US',
  level:      42,
  device:     'mobile',
});
getPlayerScores

Full submission history for a player, ordered newest first. Use for progression charts or run-history screens.

// Full submission history for a player, newest first.
// Use this for progression charts or run-history screens.
const { entries, bestScore, total } = await lb.getPlayerScores('player-123', {
  limit:  50,  // 1–200, default 50
  offset: 0,
});

console.log(`${total} runs, personal best: ${bestScore}`);
7

Score signing (anti-cheat)

Enable "Require signed scores" in your game's dashboard. Pass scoreSecret to the constructor and the SDK signs every submission automatically using HMAC-SHA256 via the Web Crypto API — no extra code needed.

// Enable score signing in your game's dashboard, then pass scoreSecret:
const lb = new LightLeaderboard({
  apiKey:      process.env.LLB_API_KEY,
  gameId:      'my_game',
  scoreSecret: process.env.LLB_SCORE_SECRET, // from dashboard → Signature Secret
});

// Scores are now automatically HMAC-SHA256 signed — no extra code needed.
// Unsigned submissions will be rejected by the server.
await lb.submitScore({ score: 9500, playerRefId: 'player-123' });
Keep scoreSecret on the server alongside apiKey. Unsigned submissions are rejected with 401 when signing is enabled.
8

Error handling

All methods throw LightLeaderboardError on failure. Use the typed helper flags to branch on common error cases.

import { LightLeaderboard, LightLeaderboardError } from 'lightleaderboard';

try {
  await lb.submitScore({ score: 9500, playerRefId: 'player-123' });
} catch (err) {
  if (err instanceof LightLeaderboardError) {
    console.error(err.message);   // human-readable message from the API
    console.error(err.status);    // HTTP status code

    if (err.isAuthError)        console.error('Check your API key');
    if (err.isRateLimitError)   console.error('Slow down score submissions');
    if (err.isBillingError)     console.error('Free tier limit — upgrade to Pro');
    if (err.isValidationError)  console.error('Invalid score data:', err.message);
  }
}

Complete Express server example

This is the pattern used in the KeyKeeper example project. The Express server holds the API key; the Kaboom.js client (running in the browser) calls your server — credentials never reach the client.

// server.js — Express example (safe: API key never touches the browser)
import express from 'express';
import { LightLeaderboard } from 'lightleaderboard';

const app = express();
app.use(express.json());

const lb = new LightLeaderboard({
  apiKey: process.env.LLB_API_KEY,
  gameId: 'my_game',
});

// Your game client POSTs here after each run
app.post('/submit', async (req, res) => {
  const { score, playerId, playerName } = req.body;
  const result = await lb.submitScore({
    score,
    playerRefId:  playerId,
    playerName,
    submissionId: crypto.randomUUID(),
  });
  res.json(result); // { rank, isPersonalBest, totalPlayers }
});

app.get('/leaderboard', async (_req, res) => {
  const data = await lb.getLeaderboard({ limit: 10, period: 'weekly' });
  res.json(data.entries);
});

app.listen(3000);