Skip to content

Leaderboard

Try the interactive example ↓

Multi-metric rankings in isolated namespaces: submit scores for several metrics at once, query top-N by a weighted composite or a single metric, look up one player's rank and neighbors, filter by time window, and batch submissions. Not just for games - sales contests, GitHub stars, and poll results rank the same way.

When to reach for it: game season rankings, sales contests, engagement scores, any "who is winning" question over one or more numeric metrics.

When not to: bracket-based competitions (single/double elimination, swiss) belong to Tournament; event-driven reputation scoring (with its own reputation leaderboard) belongs to Reputation.

Concepts

  • Namespace - an isolated leaderboard space (season-1, daily-sales). Created on the first authorized submit and private to that creator by default. Readers and editors can be shared explicitly or through a workspace.
  • Metric - one scored dimension (score, accuracy, revenue). A namespace holds several; each submit updates only the metrics it names.
  • Composite score - rankings combine a namespace's metrics into one number: the weighted sum of each entity's metric values, equal weights by default. Weights are supplied per query, not stored.
  • Entity ID - who the score belongs to. Defaults to the JWT subject, so two users submitting without -i never collide; pass -i to submit on behalf of any ID.
  • Manifest - an optional metadata record (name, description, tags) for cataloging leaderboards. Manifests and namespaces are separate systems: creating a manifest also creates its owned namespace. Submitting scores still does not require a manifest. The access record exists immediately; namespace enumeration begins with the first score. Resetting scores preserves an existing manifest and its grants, while clearing its score registration.

Enterprise access

The namespace is the permission root for its manifest, metrics, entities and scores. A reader can query rankings and metadata; an editor can submit/delete scores and edit manifest metadata; a manager can share or delete the namespace. Default entity attribution is a convenience, not permission to write into another user's namespace. Cross-namespace aggregations require access to every source. Counts, enumeration and profile includes observe the same read boundary.

snug leaderboard create accepts --workspace-id and --visibility private|workspace. The existing shared access API uses target leaderboards and the namespace as its resource ID, including namespaces created without manifests. Groups, expiring grants and service identities use the same grant machinery as other shared resources.

Submitting scores

snug leaderboard submit -n season-1 -m score:100,accuracy:80 -d '{"clan":"wolves"}'
snug leaderboard submit -n season-1 -i player-bob -m score:120,accuracy:40

A submit replaces each named metric's value; it does not add to it. Unnamed metrics survive - verified: after score:100,accuracy:80 a second submit of score:50 left accuracy at 80 and the composite at 130. There is no increment operation; to accumulate, read the current value and resubmit.

The response reports the entity's new standing:

{
  "id": "player-bob",
  "weighted_score": 160.0,
  "rank": 2,
  "total_entities": 2
}

Batch submission

batch-submit posts a JSON array of {id, metrics, meta} objects in one request - here every id is required, there is no JWT-subject default. Entries are applied independently and the batch is capped (500 by default).

snug leaderboard batch-submit -n season-1 -f players.json

Rankings

snug leaderboard top -n season-1 --limit 10                     # composite, equal weights
snug leaderboard top -n season-1 --limit 10 -w score:0.7,accuracy:0.3
snug leaderboard metric-top -n season-1 -m accuracy --limit 10  # one metric only
snug leaderboard top -n season-1 --page 2                       # ranks 11-20
snug leaderboard rank -n season-1 -i player-bob            # one entity's standing
snug leaderboard surrounding -n season-1 -i player-bob -w 2 # neighbors (-g for weights here)

Weights are relative and need not sum to 1. The arithmetic, with alice at score:100, accuracy:80 and bob at score:120, accuracy:40: equal weights rank alice first (180 vs 160), but -w score:0.7,accuracy:0.3 flips the order - bob scores 120*0.7 + 40*0.3 = 96 against alice's 94. Entities with equal composites rank in reverse id order, the same way on every ranking endpoint, so paging walks a fixed order.

Over HTTP the same query is GET /api/v1/leaderboard/{namespace}/top:

curl -H "Authorization: Bearer $SNUG_API_TOKEN" \
  "http://localhost:4000/api/v1/leaderboard/season-1/top?limit=2"
{
  "status": 200,
  "msg": "OK",
  "data": {
    "entries": [
      {
        "id": "player-alice",
        "rank": 1,
        "score": 180.0,
        "metrics": { "score": 100.0, "accuracy": 80.0 },
        "meta": { "clan": "wolves" }
      },
      {
        "id": "player-bob",
        "rank": 2,
        "score": 160.0,
        "metrics": { "score": 120.0, "accuracy": 40.0 }
      }
    ],
    "page": { "has_more": false }
  }
}

score is the composite; metrics holds the raw values. The ranking endpoints (top, {metric}/top, time-window, aggregate) share the standard list contract for paging only: limit (10 by default, 100 at most) with cursor from page.next_cursor or a 1-based page. They are uncounted, so total=true is a 400, and offset is refused like everywhere else. Querying a namespace nobody has submitted to returns empty entries with 200, not a 404. rank for an unknown entity is 200 with data: null over HTTP; the CLI prints a not-found notice and exits non-zero.

Time windows

time-window ranks only entities whose last submission falls inside a Unix epoch range, in seconds - millisecond timestamps do not error, they silently match nothing. Without -s the window starts 24 hours before its end, and without -e it ends now:

snug leaderboard time-window -n season-1 -s 1787920000 -e 1787923700 --limit 25

aggregate (GET /api/v1/leaderboard/aggregate?namespaces=a,b&metrics=score) ranks entities by their summed metrics across several namespaces, or, with group_by, groups one namespace's entities by a metadata field and sums their metrics per group:

snug leaderboard aggregate -n season-1,season-2 -m score
snug leaderboard aggregate -n season-1 -m score -g clan

group_by takes exactly one namespace and no weights; either mistake is a 400.

Manifests

snug leaderboard create --name "Season 1" --description "Spring" --tags game:arena,season:1
snug leaderboard list --query season --filter 'active[eq]=true' --sort -created_at
snug leaderboard get --id jgYBepqMbqVduynCZGAp
snug leaderboard update --id jgYBepqMbqVduynCZGAp --active false
snug leaderboard delete --id jgYBepqMbqVduynCZGAp --force

Manifests inherit their namespace authority. Updates require an editor; deletion requires a manager. A caller without the required authority is denied, and an unreadable or missing manifest is 404 leaderboard_not_found. Tags are key:value pairs, unlike the plain labels in KV.

Introspection and cleanup

snug leaderboard namespaces               # readable namespaces + entity counts
snug leaderboard metrics -n season-1      # per-metric count/min/max/avg
snug leaderboard delete-entity -n season-1 -i player-bob   # from ALL metrics
snug leaderboard delete-metric -n season-1 -m accuracy     # for ALL entities
snug leaderboard delete-namespace -n season-1 --force      # everything, no undo

There is no way to remove one entity from a single metric. Deleting a metric immediately changes composite rankings - the remaining metrics carry the whole score.

Limits and configuration

By default 100 namespaces, 10 metrics per namespace (an 11-metric submit fails with 429 metrics_limit_exceeded, verified), 1000 entries per metric, and 500 submissions per batch - all tunable via the LEADERBOARD_* variables.

Reference

Interactive example

Submit scores and rank participants using the metrics your competition needs. Look up a player’s rank or the participants around them.

An illustrative example that runs in your browser. No account or API calls required.