Leaderboards let players compete by ranking scores on shared scoreboards. Each leaderboard has a name you pick (e.g. "speedrun-times"), a sort direction, and an update policy.
Name vs. ID. Only getLeaderboard(name) and getOrCreateLeaderboard(name, ...) take the leaderboard's name. Every other call — uploadLeaderboardScore, listLeaderboardEntries, getMyLeaderboardEntries, etc. — takes the ID returned from those lookups (response.data.id)
Getting or creating a leaderboard
Call this once per leaderboard (for example during startup) and reuse the returned ID for every other leaderboard call.
func setup_leaderboard():
var leaderboard = await WavedashSDK.get_or_create_leaderboard(
"speedrun-times",
WavedashConstants.LEADERBOARD_SORT_ASCENDING,
WavedashConstants.LEADERBOARD_DISPLAY_TYPE_TIME_MILLISECONDS
)
var leaderboard_id = leaderboard.data.id if leaderboard.success else ""
print("Leaderboard ID: ", leaderboard_id)
var leaderboard = await Wavedash.SDK.GetOrCreateLeaderboard(
"speedrun-times",
sortMethod: WavedashConstants.LeaderboardSortMethod.ASCENDING,
displayType: WavedashConstants.LeaderboardDisplayType.TIME_MILLISECONDS
);
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;
Debug.Log($"Leaderboard ID: {leaderboardId}");
local co = coroutine.create(function()
local leaderboard = wavedash.get_or_create_leaderboard_async(
"speedrun-times",
wavedash.LEADERBOARD_SORT_ORDER_ASC,
wavedash.LEADERBOARD_DISPLAY_TYPETIME_MILLISECONDS
)
local leaderboard_id = leaderboard.success and leaderboard.data.id or nil
print("Leaderboard ID:", leaderboard_id)
end)
assert(coroutine.resume(co))
const leaderboard = await Wavedash.getOrCreateLeaderboard(
"speedrun-times",
Wavedash.LeaderboardSortOrder.ASC,
Wavedash.LeaderboardDisplayType.TIME_MILLISECONDS
);
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;
If the leaderboard already exists and you just need its ID, use getLeaderboard(name) instead — it fails if the leaderboard doesn't exist yet.
Godot signal alternative. If you prefer signal-based flow over await, every leaderboard call also emits a signal when its response arrives:
func _ready():
WavedashSDK.got_leaderboard.connect(_on_got_leaderboard)
WavedashSDK.got_leaderboard_entries.connect(_on_got_entries)
WavedashSDK.posted_leaderboard_score.connect(_on_posted_score)
func _on_got_leaderboard(response):
print("ID: ", response.data.id if response.success else "?")
func _on_got_entries(response):
if response.success:
for entry in response.data:
print(entry.globalRank, ": ", entry.score)
func _on_posted_score(response):
if response.success:
print("Rank: ", response.data.globalRank)
Sort order
| Value | Constant | Description | Best for |
|---|---|---|---|
0 | ASC | Lower scores rank higher | Time trials, golf |
1 | DESC | Higher scores rank higher | Points, high scores |
In JavaScript use Wavedash.LeaderboardSortOrder.ASC / DESC. In Godot use WavedashConstants.LEADERBOARD_SORT_ASCENDING / LEADERBOARD_SORT_DESCENDING. In Unity use WavedashConstants.LeaderboardSortMethod.ASCENDING / DESCENDING. In Defold use wavedash.LEADERBOARD_SORT_ORDER_ASC / LEADERBOARD_SORT_ORDER_DESC.
Display type
| Value | Constant | Description |
|---|---|---|
0 | NUMERIC | Display as a number |
1 | TIME_SECONDS | Display as time in seconds |
2 | TIME_MILLISECONDS | Display as time with milliseconds |
3 | TIME_GAME_TICKS | Display as game ticks (assumes 60fps) |
In JavaScript use Wavedash.LeaderboardDisplayType.NUMERIC / TIME_SECONDS / TIME_MILLISECONDS / TIME_GAME_TICKS. In Defold use wavedash.LEADERBOARD_DISPLAY_TYPENUMERIC / LEADERBOARD_DISPLAY_TYPETIME_SECONDS / wavedash.LEADERBOARD_DISPLAY_TYPETIME_MILLISECONDS / wavedash.LEADERBOARD_DISPLAY_TYPETIME_GAME_TICKS.
Visibility
Every leaderboard is either Visible — shown publicly on your game page's Leaderboards tab — or Hidden.
- Leaderboards created by you (from the developer portal, or via
getOrCreateLeaderboardwhile playing as a member of your game's team) default to Visible. - Leaderboards created by your players default to Hidden, so clients calling
getOrCreateLeaderboardwith arbitrary names can't spam your game page.
You can toggle visibility at any time from the developer portal's Leaderboards tab, from your game page (team members can view and toggle hidden leaderboards on the game page), or from your own server with the HTTP API.
Display name, sort order, and display type can also be changed after creation from your own server with the HTTP API.
Submitting a score
Resolve the name to an ID first, then pass that ID to uploadLeaderboardScore.
func submit_score(score: int):
var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
if not leaderboard.success:
return
var leaderboard_id = leaderboard.data.id
var result = await WavedashSDK.post_leaderboard_score(leaderboard_id, score, true)
if result.success:
print("Your leaderboard rank: ", result.data.globalRank)
# Where this run placed, even if your leaderboard standing didn't change
print("This run ranked: ", result.data.submittedRank)
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;
var result = await Wavedash.SDK.UploadLeaderboardScore(
leaderboardId, score, keepBest: true
);
if (result != null)
{
Debug.Log($"Your leaderboard rank: {result["globalRank"]}");
// Where this run placed, even if your leaderboard standing didn't change
Debug.Log($"This run ranked: {result["submittedRank"]}");
}
local co = coroutine.create(function()
local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
if not leaderboard.success then
return
end
local leaderboard_id = leaderboard.data.id
local result = wavedash.upload_leaderboard_score_async(leaderboard_id, score, true)
if result.success then
print("Your leaderboard rank:", result.data.globalRank)
-- Where this run placed, even if your leaderboard standing didn't change
print("This run ranked:", result.data.submittedRank)
end
end)
assert(coroutine.resume(co))
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;
const response = await Wavedash.uploadLeaderboardScore(
leaderboardId, 1500, true
);
if (response.success) {
console.log(`Your rank: ${response.data.globalRank}`);
// Where this run placed, even if your saved standing didn't change
console.log(`This run ranked: ${response.data.submittedRank}`);
}
| Parameter | Type | Required | Description |
|---|---|---|---|
leaderboardId | Id<"leaderboards"> | Yes | The leaderboard's ID (from getLeaderboard / getOrCreateLeaderboard) |
score | number | Yes | The score to submit |
keepBest | boolean | Yes | If true, only updates if score is better |
ugcId | Id<"userGeneratedContent"> | No | Optional UGC attachment (replay, screenshot) |
metadata | object | No | Small key/value data to store with the entry — see Attaching metadata |
Use keepBest: true for competitive leaderboards. The SDK handles personal best tracking automatically.
Fetching scores
Top scores
func get_top_scores():
var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
if not leaderboard.success:
return
var leaderboard_id = leaderboard.data.id
var response = await WavedashSDK.get_leaderboard_entries(leaderboard_id, 0, 10, false)
if response.success:
for entry in response.data:
print("#", entry.globalRank, " ", entry.username, ": ", entry.score)
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;
var entries = await Wavedash.SDK.ListLeaderboardEntries(leaderboardId, 0, 10);
foreach (var entry in entries)
Debug.Log($"#{entry["globalRank"]} {entry["username"]}: {entry["score"]}");
local co = coroutine.create(function()
local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
if not leaderboard.success then
return
end
local leaderboard_id = leaderboard.data.id
local response = wavedash.list_leaderboard_entries_async(leaderboard_id, 0, 10, false)
if response.success then
for _, entry in ipairs(response.data) do
print("#" .. entry.globalRank, entry.username .. ":", entry.score)
end
end
end)
assert(coroutine.resume(co))
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;
const response = await Wavedash.listLeaderboardEntries(
leaderboardId, 0, 10, false
);
if (response.success) {
response.data.forEach(entry => {
console.log(`#${entry.globalRank} ${entry.username}: ${entry.score}`);
});
}
Nearby scores
func get_scores_around_me():
var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
if not leaderboard.success:
return
var leaderboard_id = leaderboard.data.id
var nearby = await WavedashSDK.get_leaderboard_entries_around_player(leaderboard_id, 5, 5, false)
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;
var nearby = await Wavedash.SDK.ListLeaderboardEntriesAroundUser(
leaderboardId, countAhead: 5, countBehind: 5
);
local co = coroutine.create(function()
local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
if not leaderboard.success then
return
end
local leaderboard_id = leaderboard.data.id
local nearby = wavedash.list_leaderboard_entries_around_user_async(leaderboard_id, 5, 5, false)
end)
assert(coroutine.resume(co))
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;
const nearby = await Wavedash.listLeaderboardEntriesAroundUser(
leaderboardId, 5, 5, false
);
Player's own entry
func get_my_entry():
var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
if not leaderboard.success:
return
var leaderboard_id = leaderboard.data.id
var entries = await WavedashSDK.get_my_leaderboard_entries(leaderboard_id)
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;
var entries = await Wavedash.SDK.GetMyLeaderboardEntries(leaderboardId);
local co = coroutine.create(function()
local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
if not leaderboard.success then
return
end
local leaderboard_id = leaderboard.data.id
local entries = wavedash.get_my_leaderboard_entries_async(leaderboard_id)
end)
assert(coroutine.resume(co))
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;
const response = await Wavedash.getMyLeaderboardEntries(leaderboardId);
Entry count
getLeaderboardEntryCount is a synchronous accessor that returns the cached total from the last fetch — but it still takes the leaderboard ID, not the name, so you must resolve the name first.
func get_count():
var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
if not leaderboard.success:
return
var leaderboard_id = leaderboard.data.id
var count = WavedashSDK.get_leaderboard_entry_count(leaderboard_id)
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;
var count = Wavedash.SDK.GetLeaderboardEntryCount(leaderboardId);
local co = coroutine.create(function()
local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
if not leaderboard.success then
return
end
local leaderboard_id = leaderboard.data.id
local count = wavedash.get_leaderboard_entry_count(leaderboard_id)
end)
assert(coroutine.resume(co))
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;
const count = Wavedash.getLeaderboardEntryCount(leaderboardId);
Returns a cached value from the last query. Returns -1 if the leaderboard has not been queried yet.
Attaching metadata to an entry
Pass a metadata map to store a little context alongside the score — the character a run used, how many deaths it took, the seed that generated the level. It comes back on every read of that entry, so your leaderboard UI can show it next to the score without a second round trip.
func submit_run(leaderboard_id: String, score: int):
var result = await WavedashSDK.post_leaderboard_score(leaderboard_id, score, true, "", {
"character": "knight",
"deaths": 3
})
if result.success:
print("Saved with character: ", result.data.metadata.character)
var result = await Wavedash.SDK.UploadLeaderboardScore(
leaderboardId, score, keepBest: true,
metadata: new Dictionary<string, object>
{
{ "character", "knight" },
{ "deaths", 3 }
}
);
if (result != null && result.TryGetValue("metadata", out var raw) && raw is JObject metadata)
{
Debug.Log($"Saved with character: {metadata["character"]}");
}
local co = coroutine.create(function()
local result = wavedash.upload_leaderboard_score_async(leaderboard_id, score, true, nil, {
character = "knight",
deaths = 3
})
if result.success then
print("Saved with character:", result.data.metadata.character)
end
end)
assert(coroutine.resume(co))
const response = await Wavedash.uploadLeaderboardScore(
leaderboardId, 1500, true, undefined, {
character: "knight",
deaths: 3
}
);
if (response.success) {
console.log(`Saved with character: ${response.data.metadata.character}`);
}
metadata is the fifth argument, after ugcId. To send metadata without a UGC attachment, pass an empty ugcId — "" in Godot, nil in Defold, undefined in JavaScript — or, in Unity, skip it with a named argument as above.
What you can store
Keys are strings, and values are strings or numbers. Booleans, nested objects, and arrays are rejected — flatten them first (store a perfect run as 1 or "yes" rather than true), or upload the payload as UGC and attach it with ugcId instead.
| Limit | Value |
|---|---|
| Keys per entry | 16 |
| Key length | 64 characters |
| String value length | 256 characters |
| Total size | 2048 bytes, for the whole map encoded as JSON |
Breaking any of these fails the upload: the response comes back with success false and a message naming the limit, and the score is not saved. Validate or clamp game-generated strings (player-entered names, level titles) before submitting them, so an oversized value can't cost a player their score.
In Unity, metadata arrives as a Newtonsoft JObject nested inside the response dictionary — add using Newtonsoft.Json.Linq; to read it. The key is absent entirely on entries with no metadata, so prefer TryGetValue over indexing.
If you're calling window.Wavedash through hand-written glue from a language that can't build a JS object literal, metadata also accepts a JSON string ('{"character":"knight"}'), which the SDK parses for you — this is how the engine SDKs pass it across the bridge. Malformed JSON fails the call with a message rather than submitting the score.
Metadata belongs to the score
Metadata is stored on the entry rather than accumulated across submissions:
- A submission that gets saved replaces the previous metadata wholesale.
- A submission that gets saved and omits
metadataclears it. - A submission that
keepBestrejects leaves the existing entry untouched, metadata included — so the metadata on a leaderboard always describes the run that set the saved score.
There's no partial update, so send the complete map with every score you submit.
Reading it back
metadata comes back on entries that have it from every read path — listLeaderboardEntries, listLeaderboardEntriesAroundUser, getMyLeaderboardEntries — as well as on the uploadLeaderboardScore response itself. The field is omitted on entries with no metadata, so guard for it:
const response = await Wavedash.listLeaderboardEntries(leaderboardId, 0, 10, false);
if (response.success) {
response.data.forEach(entry => {
const character = entry.metadata?.character ?? "unknown";
console.log(`#${entry.globalRank} ${entry.username}: ${entry.score} (${character})`);
});
}
Attaching replays or screenshots
For anything bigger than a handful of key/value pairs — a replay file, a screenshot, a ghost recording — upload it as UGC and attach it instead of squeezing it into metadata.
Pass a ugcId from UGC into the score upload call. Note that uploadLeaderboardScore takes lb.data.id (the ID), not the name:
const lb = await Wavedash.getOrCreateLeaderboard(
"speedrun-times",
Wavedash.LeaderboardSortOrder.ASC,
Wavedash.LeaderboardDisplayType.TIME_MILLISECONDS
);
const ugc = await Wavedash.createUGCItem(
Wavedash.UGCType.GAME_MANAGED,
"Replay",
"replay of level 1",
Wavedash.UGCVisibility.PUBLIC,
"replays/run.dat"
);
if (lb.success && ugc.success) {
await Wavedash.uploadLeaderboardScore(lb.data.id, 12345, true, ugc.data);
}
Return types
Leaderboard
Returned from getLeaderboard and getOrCreateLeaderboard. Use id for every subsequent leaderboard call.
interface Leaderboard {
id: Id<"leaderboards">; // pass this to uploadLeaderboardScore, listLeaderboardEntries, etc.
name: string;
totalEntries: number;
created?: boolean; // only set by getOrCreateLeaderboard — true if newly created
}
LeaderboardEntry
An entry in the list returned by listLeaderboardEntries, listLeaderboardEntriesAroundUser, and getMyLeaderboardEntries.
interface LeaderboardEntry {
userId: Id<"users">;
username: string;
userAvatarUrl?: string;
score: number;
globalRank: number;
timestamp: number;
metadata?: Record<string, string | number>; // omitted if the score was submitted without metadata
ugcId?: Id<"userGeneratedContent">;
}
UpsertedLeaderboardEntry
Returned from uploadLeaderboardScore. Note that this is a different shape from LeaderboardEntry — it reports what changed on upload rather than the full entry.
It carries two pairs of fields: your saved standing (score / globalRank) and the submitted attempt (submittedScore / submittedRank). These differ when keepBest is true and the run you just submitted was worse than your saved best — score/globalRank stay at your best, while submittedScore/submittedRank reflect where this particular run would have placed.
interface UpsertedLeaderboardEntry {
entryId: Id<"leaderboardEntries">;
score: number; // your saved standing's score
scoreChanged: boolean; // false if keepBest was true and the pre-existing score was better
globalRank: number; // your saved standing's rank
submittedScore: number; // the score of the run you just submitted
submittedRank: number; // where that submitted run would rank
metadata?: Record<string, string | number>; // the metadata now stored on the entry
userId: Id<"users">;
username: string;
userAvatarUrl?: string;
}