API Docs

GameSDK saves & scores reference

AdminUpdated Sep 22, 2026

GameSDK saves & scores reference

This page is the reference for the GameSDK methods that handle cloud saves, scores, leaderboards and replay verification. For explanations and patterns, see Saves and progress and Scores, leaderboards & anti-cheat. For loading the SDK, ready(), start() and events, see GameSDK core reference.

Conventions

  • Fire-and-forget methods return undefined. They don't report success or failure.

  • Promise methods never reject, and every one has a built-in timeout, so they always settle. On error or timeout they resolve with a fallback value, listed under each method.

  • Signed-in means the player is signed in to Cool GPT Games. For guests, some methods do nothing or return empty results, as listed under each method.

  • Outside Cool GPT Games (for example, opening your bundle directly during development), no page answers SDK messages. Timeouts and fallbacks behave as listed under each method.

  • Runtimes: these methods work in any game that loads the GameSDK script and runs on Cool GPT Games. See How games run for which runtimes include the SDK. - Message rate: all SDK calls share a budget of about 30 messages per second to the site. Messages over the budget are dropped silently, and a dropped request's promise resolves with its timeout value.

Summary

Method

Returns

Signed-in required

saveData(key, value)

Promise<SaveResult>

No (guests save locally)

loadData(key)

Promise<string | null>

No (guests load locally)

listSaves()

Promise<SaveSlot[]>

No (guests list locally)

deleteSave(key)

void

No (guests delete locally)

submitScore(score, nonce?)

void

Yes. Ignored for guests.

gameOver(score?)

void

No

getMyScore()

Promise<{ score, rank }>

Yes. Guests get nulls.

social.getLeaderboard(scope?)

Promise<LeaderboardRow[]>

Only for "friends"

replay.isVerifying()

boolean

No

replay.ready()

Promise<string>

No

replay.seed()

string | null

No

replay.inputs()

unknown[]

No

replay.record(step)

void

No

replay.reset()

void

No

replay.submit(score)

void

Yes in live play. Ignored for guests.

on("replaySeed", cb)

GameSDK

No


Saves

saveData

GameSDK.saveData(key: string, value: string): Promise<SaveResult>

type SaveResult =
  | { ok: true; storage: "cloud" | "local" }
  | { ok: false; error: string };

Writes value to the slot key for the current player and game. The write goes to the player's account if they're signed in, or to this browser's local storage if they're a guest. It replaces any existing value for the key (last write wins).

Parameters

Name

Type

Required

Constraints

key

string

Yes

Converted with String(key). Must be 1–128 characters for signed-in players.

value

string

Yes

Converted with String(value), so pass a string (use JSON.stringify for objects). At most 65,536 UTF-8 bytes.

Resolves with what happened to the write. You can ignore the promise if you don't need to know.

Result

Meaning

{ ok: true, storage: "cloud" }

Saved to the signed-in player's account.

{ ok: true, storage: "local" }

Saved to this browser only. Always the case for guests. For a signed-in player it means the cloud couldn't be reached (network error), so the value was kept on this device instead.

{ ok: false, error }

Not saved. error says why (see below).

error

Cause

Players affected

"too_large"

Value over 65,536 UTF-8 bytes. Checked before anything is sent.

All

"validation_error"

Empty key, or key over 128 characters

Signed in

"too_many_saves"

A new key when the player already has 100 slots for this game

Signed in

"rate_limited"

More than 120 writes per minute for this player and game

Signed in

Another API error code

The account save was refused for another reason (for example, the player's sign-in expired)

Signed in

"storage_unavailable"

This browser's storage is full or blocked

Guests, and signed-in players whose cloud save hit a network error

"timeout"

No answer within 8 seconds (for example, the message was dropped by the 30-per-second budget, or you're outside Cool GPT Games)

All

A value kept locally for a signed-in player (storage: "local") isn't in their account. Cloud reads on other devices don't see it, and on this device loadData returns it only when the cloud can't be reached. Save again later if you need it in the account.

Sign-in: not required. Guests save to local storage on the site. That data is tied to the browser and moves into the account on sign-in (keys that already exist in the cloud keep their cloud value).

Limits: 64 KB per value · 128-character keys · 100 slots per player per game · 120 writes per minute per player per game.

Example

const state = { v: 2, level: 7, coins: 340 };
const res = await GameSDK.saveData("progress", JSON.stringify(state));
if (!res.ok) {
  showToast(res.error === "too_many_saves" ? "Too many save slots. Delete one first." : "Couldn't save");
}

Pitfalls

  • Await the result instead of calling loadData straight after saveData to confirm the write. Keep your game state in memory as the source of truth.

  • Don't create keys from dynamic data (timestamps, player input). The 100-slot limit is permanent until you delete keys.

  • Your game can't use window.localStorage itself. The game runs in a sandbox, so use this method instead.


loadData

GameSDK.loadData(key: string): Promise<string | null>

Reads the slot key for the current player and game.

Parameters

Name

Type

Required

Constraints

key

string

Yes

Converted with String(key). Up to 128 characters for signed-in players.

Resolves with the stored string, or null if nothing was found.

Situation

Resolves with

Signed in, key exists

The cloud value

Signed in, key doesn't exist

null

Signed in, cloud request fails (network or non-OK response)

This browser's local value for the key, usually null

Guest

This browser's local value, or null

No answer within 8 seconds (outside Cool GPT Games, or the message was dropped)

null

Failure behaviour: never rejects. Resolves null after 8 seconds if the page doesn't answer.

Sign-in: not required.

Example

const raw = await GameSDK.loadData("progress");
let state;
try { state = raw ? JSON.parse(raw) : { v: 2, level: 1, coins: 0 }; }
catch { state = { v: 2, level: 1, coins: 0 }; }

Pitfalls

  • null can mean "the cloud couldn't be reached" or "no answer", not only "no save". Before a fresh state overwrites a real save, check with listSaves(). See the "Protect existing progress" pattern in Saves and progress.

  • Parallel calls, including for the same key, each resolve with their own answer.

  • In replay verify mode there's no page to answer, so it resolves null after the 8-second timeout. Don't await it before your verify path.


listSaves

GameSDK.listSaves(): Promise<SaveSlot[]>

type SaveSlot = {
  key: string;
  updatedAt?: string; // ISO-8601 timestamp — signed-in players only
};

Lists the current player's save slots for this game.

Parameters: none.

Resolves

Situation

Resolves with

Signed in

[{ key, updatedAt }], up to 100 entries

Guest

[{ key }] (no updatedAt)

Error, or no answer within 6 seconds

[]

Entries come back in no guaranteed order.

Failure behaviour: never rejects. Resolves [] on error or timeout, which looks the same as "no saves".

Sign-in: not required.

Example

const slots = await GameSDK.listSaves();
slots.sort((a, b) => a.key.localeCompare(b.key));
for (const s of slots) {
  console.log(s.key, s.updatedAt ? new Date(s.updatedAt).toLocaleString() : "(this device)");
}

Pitfalls

  • If you need a "last saved" time for guests too, store your own timestamp inside the value.

  • A listSaves() sent straight after deleteSave can come back before the delete finishes. Await saveData before listing, and update your UI optimistically after a delete.


deleteSave

GameSDK.deleteSave(key: string): void

Deletes the slot key for the current player and game. This frees one of the 100 slots.

Parameters

Name

Type

Required

Constraints

key

string

Yes

Converted with String(key). Up to 128 characters for signed-in players.

Returns undefined.

Failure behaviour: silent. Deleting a key that doesn't exist succeeds without doing anything.

Sign-in: not required. For signed-in players only the cloud copy is deleted. For guests the local copy is deleted.

Example

GameSDK.deleteSave("slot2");

Pitfalls: you get no confirmation. Remove the slot from your UI right away rather than waiting for listSaves().


Scores and leaderboards

submitScore

GameSDK.submitScore(score: number, nonce?: string): void

Submits a score for the signed-in player to this game's leaderboard. The leaderboard keeps each player's best score, and higher is better. A new personal best shows a "New personal best!" toast and grants a small XP bonus. The score also counts toward any live tournament on the game.

Parameters

Name

Type

Required

Constraints

score

number

Yes

Must be a JavaScript number. Anything else is ignored. Rounded down with Math.floor. Must be between 0 and 1,000,000,000,000.

nonce

string

No

Accepted but currently ignored by the site.

Returns undefined. To show the result, call getMyScore() afterwards.

When a score is recorded. All of these must hold. Otherwise the score is dropped silently:

Requirement

Detail

Signed in

Guest submissions are ignored. The site may show the guest a sign-in prompt.

Play session started

Your game called GameSDK.start() or GameSDK.replay.ready() earlier on this page

Enough playtime

At least 4 counted heartbeats, about 60 seconds since the session started

Session valid

The session hasn't ended and wasn't flagged at start. A browser with more than 20 sessions of this game in 24 hours gets flagged sessions.

Valid number

Finite, 0 ≤ score ≤ 10¹²

Rate limit

30 submissions per minute per player per game, shared with replay.submit

Plausibility. Once your game has 10 or more players on the leaderboard, a score above 5× the leaderboard's 95th percentile, or more than 5× the fastest points-per-second rate, is held for review and kept off the leaderboard. The player isn't told.

Sign-in: required. Guests get no error.

Embedding: when your game runs in the embeddable player, every numeric submitScore call (including from guests) is also forwarded to the host page as a score event. See Embedding & the score bridge.

Example

function onRunEnd(finalScore) {
  GameSDK.submitScore(finalScore);
  GameSDK.gameOver(finalScore);
  setTimeout(async () => {
    const { score, rank } = await GameSDK.getMyScore();
    if (score !== null) showBest(score, rank);
  }, 1000);
}

Pitfalls

  • gameOver(score) does not submit a score.

  • If you never call start() (or replay.ready()), nothing is recorded.

  • A run that ends less than about a minute after start() isn't recorded. Submit every run anyway, because later runs in the same session will qualify.

  • If you use replay verification, use replay.submit instead of submitScore for that run.


gameOver

GameSDK.gameOver(score?: number): void

Signals that a run has ended. When your game is embedded on another site, this sends a gameover event (with score, if it's a number) to the host page.

Parameters

Name

Type

Required

Constraints

score

number

No

Included in the host event only if it is a number

Returns undefined.

Failure behaviour: none.

Sign-in: not required.

Example

GameSDK.gameOver(1200);

Pitfalls: it doesn't record a leaderboard score and doesn't end the play session. Call submitScore (or replay.submit) as well.


getMyScore

GameSDK.getMyScore(): Promise<{ score: number | null; rank: number | null }>

Returns the signed-in player's best score on this game's public leaderboard and their rank.

Parameters: none.

Resolves

Field

Type

Meaning

score

number | null

The player's best score, or null if they have none

rank

number | null

1 + the number of players with a strictly higher best. Tied players share a rank.

Resolves { score: null, rank: null } for guests, players with no score, any error, or no answer within 6 seconds.

Failure behaviour: never rejects.

Sign-in: required to get values. Guests always get nulls.

Example

const { score, rank } = await GameSDK.getMyScore();
bestLabel.textContent = score === null ? "No best yet" : `Best ${score} · #${rank}`;

Pitfalls

  • The value reflects the public leaderboard, not the verified one.

  • Right after submitScore, the new score may not be stored yet. Wait a moment before reading.

  • A score held for review is not included.


social.getLeaderboard

GameSDK.social.getLeaderboard(scope?: "global" | "friends"): Promise<LeaderboardRow[]>

type LeaderboardRow = {
  rank: number;              // 1-based position in this list
  handle: string;
  displayName: string | null;
  avatarUrl: string | null;
  level: number;             // the player's Cool GPT Games level
  score: number;             // their best score on this game
};

Returns this game's all-time public leaderboard: the top 50 players by best score, highest first.

Parameters

Name

Type

Required

Constraints

scope

"global" | "friends"

No

Default "global". Any value other than "friends" is treated as "global".

Scope

Contents

"global"

All players

"friends"

The signed-in player plus their friends. rank is the position within that list.

Resolves with up to 50 rows. It resolves [] on error, on no answer within 6 seconds, or for a guest who asks for "friends".

Failure behaviour: never rejects.

Sign-in: not required for "global". Required for "friends".

Example

const rows = await GameSDK.social.getLeaderboard("global");
if (rows.length === 0) showEmpty("No scores yet — be the first!");
for (const r of rows) {
  addRow(`#${r.rank}`, r.displayName || r.handle, r.score.toLocaleString());
}

Pitfalls

  • Ranks here are consecutive (1, 2, 3...) even when scores tie, while getMyScore gives tied players the same rank. If you show both, a tied player can see two different ranks.

  • Weekly, monthly and verified leaderboards aren't available through the SDK. Use the leaderboard REST endpoint (see REST API overview).

  • [] can mean "empty" or "failed". Word your empty state neutrally.


Replay verification

These methods live on GameSDK.replay. For how the system works, the determinism rules and a full example, see Scores, leaderboards & anti-cheat.

Live play means the player is playing on Cool GPT Games. Verify mode means the server is re-running your bundle in a headless browser to check a score.

replay.isVerifying

GameSDK.replay.isVerifying(): boolean

Returns true when your bundle is running in verify mode, and false in live play. The value is fixed when the SDK loads and never changes during the page's lifetime.

Parameters: none. Sign-in: not applicable. Failure behaviour: none.

Example

if (GameSDK.replay.isVerifying()) {
  runHeadless();   // simulate the recorded inputs, then replay.submit(score)
} else {
  runLive();
}

Pitfalls: in verify mode, don't read live input, Date/performance.now(), Math.random() or remote data inside your simulation.


replay.ready

GameSDK.replay.ready(): Promise<string>

Resolves with the seed for your game's random number generator.

Mode

Resolves with

Verify mode

The seed recorded for the session being verified. Resolves immediately.

Live play, seed already received

The server-issued seed for this play session. Resolves immediately.

Live play, no seed yet

Asks the site, which starts the play session if needed and returns the session's server-issued seed

Live play, no seed within 6 seconds

A local fallback seed "local.<timestamp>". That run can't be verified.

Server-issued seeds are 32-character hexadecimal strings. There's one seed per play session.

Parameters: none.

Failure behaviour: never rejects. Falls back to a local seed after 6 seconds.

Sign-in: not required. Guests also get a server seed, but their replays aren't submitted (see replay.submit).

Example

GameSDK.start();
const seed = await GameSDK.replay.ready();
const rng = mulberry32(hashSeed(seed));

start() and replay.ready() can be called in either order, or at the same moment. They share one play session, so the seed always belongs to the session your replay is verified against.

Pitfalls

  • Call it once when the page loads, and reuse the seed for every run in the session. To vary runs, derive a seed for each one (see replay.reset).

  • A seed that starts with local. means this run can't be verified.


replay.seed

GameSDK.replay.seed(): string | null

Returns the current seed synchronously. In verify mode this is the recorded seed. In live play it's null until the seed arrives from the site.

Parameters: none. Failure behaviour: none. Sign-in: not applicable.

Example

const s = GameSDK.replay.seed() ?? (await GameSDK.replay.ready());

Pitfalls: prefer replay.ready(). seed() doesn't request a seed and doesn't wait for one.


replay.inputs

GameSDK.replay.inputs(): unknown[]

Verify mode: returns the ordered array of steps recorded with replay.record() in live play, after a JSON round trip. Live play: returns [].

Parameters: none. Failure behaviour: none. Sign-in: not applicable.

Example

const steps = GameSDK.replay.inputs();
const runIndex = steps[0];
for (let i = 1; i < steps.length && !sim.over; i++) step(steps[i]);

Pitfalls: each step goes through a JSON round trip, which changes some values. undefined and NaN become null, functions and undefined properties disappear, and Date objects become strings. Simulate with the exact serialized form, so record plain numbers, strings, booleans, arrays and simple objects.


replay.record

GameSDK.replay.record(step: unknown): void

Live play: appends one step to this run's input log. Verify mode: does nothing.

Parameters

Name

Type

Required

Constraints

step

any JSON-serializable value

Yes

Keep it small. The whole log, serialized as a JSON array, must be ≤ 512 KB.

Returns undefined.

Limits: 200,000 steps per log. Steps after that are dropped silently. The serialized log must be ≤ 512 KB, or replay.submit is rejected in full, including the public-leaderboard score.

Failure behaviour: silent.

Sign-in: not required to record.

Example

const input = readInput();         // e.g. -1, 0 or 1
GameSDK.replay.record(input);      // record exactly what the step will use
step(input);

Pitfalls

  • Record the exact value your step function uses. If you record a rounded value but simulate with the unrounded one, the replay drifts and is rejected.

  • Record once per fixed simulation step, not once per animation frame.

  • The log is held in memory and isn't cleared automatically between runs. Call replay.reset() when a new run starts.


replay.reset

GameSDK.replay.reset(): void

Live play: clears the recorded input log so a new run starts empty, for example on "play again". Verify mode: does nothing. The server seed doesn't change.

Parameters: none. Returns undefined. Failure behaviour: none. Sign-in: not applicable.

Example

function startRun() {
  runIndex++;
  GameSDK.replay.reset();
  GameSDK.replay.record(runIndex);            // step 0: lets verify mode rebuild the seed
  rng = mulberry32(hashSeed(sessionSeed + ":" + runIndex));
}

Pitfalls: if you don't reset between runs, the next submission contains every earlier run's inputs as well, and the re-simulation won't match.


replay.submit

GameSDK.replay.submit(score: number): void

Ends the run.

Mode

Effect

Live play

Sends score and the recorded input log to the site. In the embeddable player it also sends a score event to the host page, like submitScore. The score is recorded on the public leaderboard under the same rules as submitScore. If the score could enter the verified top 50 and beats the player's own verified best, the server queues a headless re-simulation with the session's server-issued seed. If the re-simulated score matches exactly, the score is promoted to the verified leaderboard.

Verify mode

Publishes score as the result of the re-simulation. Nothing else is sent.

Parameters

Name

Type

Required

Constraints

score

number

Yes

Converted with Number(score) || 0. The server rounds down with Math.floor before comparing. The same 0 ≤ score ≤ 10¹² range as submitScore applies to the public leaderboard.

Returns undefined. Your game isn't told the verification result.

Failure behaviour: silent. In live play the submission is dropped when:

  • the player is a guest,

  • no play session was started,

  • the session has had less than about 60 seconds of playtime,

  • the rate limit is exceeded (30 per minute, shared with submitScore), or

  • the serialized input log is over 512 KB.

A replay that doesn't match, fails, or isn't queued leaves the score on the public leaderboard only. A score is never verified wrongly.

Sign-in: required in live play.

Limits: the headless replay verifier is enabled in production. It gives the verify-mode run about 60 seconds to call submit() after the page loads. It runs headless Chromium at 640 × 480.

Example

// Live
if (sim.over) {
  GameSDK.replay.submit(sim.score);
  GameSDK.gameOver(sim.score);
}

// Verify mode
for (let i = 1; i < inputs.length && !sim.over; i++) step(inputs[i]);
GameSDK.replay.submit(sim.score);

Pitfalls

  • Call replay.submit or submitScore for a run, not both. Calling both records the run twice, which uses up the rate limit and double-counts in tournaments that add up scores.

  • Like submitScore, it sends a score event to an embedding page. Call gameOver(score) too if the host page also listens for gameover.

  • In verify mode, call it exactly once. Run the simulation in a plain loop so it finishes inside the time budget.


Event: replaySeed

GameSDK.on("replaySeed", (seed: string) => void): GameSDK

Fires in live play when the site sends the session's server-issued seed. That happens after replay.ready() asks for it, and also when GameSDK.start() starts the session. It never fires in verify mode.

Example

GameSDK.on("replaySeed", (seed) => console.log("server seed", seed));

Pitfalls: it can fire more than once. Seed your RNG from the value replay.ready() resolves with and don't reseed a run that's already running.


Timeouts at a glance

Method

Timeout

Value on timeout

saveData

8 s

{ ok: false, error: "timeout" }

loadData

8 s

null

listSaves

6 s

[]

getMyScore

6 s

{ score: null, rank: null }

social.getLeaderboard

6 s

[]

replay.ready

6 s

"local.<timestamp>"

Related

Was this page helpful?