GameSDK saves & scores reference
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
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 |
|---|---|---|
| No (guests save locally) | |
| No (guests load locally) | |
| No (guests list locally) | |
| No (guests delete locally) | |
| Yes. Ignored for guests. | |
| No | |
| Yes. Guests get nulls. | |
| Only for | |
| No | |
| No | |
| No | |
| No | |
| No | |
| No | |
| Yes in live play. Ignored for guests. | |
| 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 |
|---|---|---|---|
|
| Yes | Converted with |
|
| Yes | Converted with |
Resolves with what happened to the write. You can ignore the promise if you don't need to know.
Result | Meaning |
|---|---|
| Saved to the signed-in player's account. |
| 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. |
| Not saved. |
| Cause | Players affected |
|---|---|---|
| Value over 65,536 UTF-8 bytes. Checked before anything is sent. | All |
| Empty key, or key over 128 characters | Signed in |
| A new key when the player already has 100 slots for this game | Signed in |
| 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 |
| This browser's storage is full or blocked | Guests, and signed-in players whose cloud save hit a network error |
| 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
loadDatastraight aftersaveDatato 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.localStorageitself. 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 |
|---|---|---|---|
|
| Yes | Converted with |
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 |
|
Signed in, cloud request fails (network or non-OK response) | This browser's local value for the key, usually |
Guest | This browser's local value, or |
No answer within 8 seconds (outside Cool GPT Games, or the message was dropped) |
|
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
nullcan mean "the cloud couldn't be reached" or "no answer", not only "no save". Before a fresh state overwrites a real save, check withlistSaves(). 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
nullafter 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 |
|
Guest |
|
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 afterdeleteSavecan come back before the delete finishes. AwaitsaveDatabefore listing, and update your UI optimistically after a delete.
deleteSave
GameSDK.deleteSave(key: string): voidDeletes the slot key for the current player and game. This frees one of the 100 slots.
Parameters
Name | Type | Required | Constraints |
|---|---|---|---|
|
| Yes | Converted with |
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): voidSubmits 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 |
|---|---|---|---|
|
| Yes | Must be a JavaScript |
|
| 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 |
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, |
Rate limit | 30 submissions per minute per player per game, shared with |
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()(orreplay.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.submitinstead ofsubmitScorefor that run.
gameOver
GameSDK.gameOver(score?: number): voidSignals 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 |
|---|---|---|---|
|
| No | Included in the host event only if it is a |
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 |
|---|---|---|
|
| The player's best score, or |
|
|
|
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 |
|---|---|---|---|
|
| No | Default |
Scope | Contents |
|---|---|
| All players |
| The signed-in player plus their friends. |
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
getMyScoregives 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(): booleanReturns 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 |
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 | nullReturns 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): voidLive play: appends one step to this run's input log. Verify mode: does nothing.
Parameters
Name | Type | Required | Constraints |
|---|---|---|---|
| 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(): voidLive 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): voidEnds the run.
Mode | Effect |
|---|---|
Live play | Sends |
Verify mode | Publishes |
Parameters
Name | Type | Required | Constraints |
|---|---|---|---|
|
| Yes | Converted with |
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), orthe 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.submitorsubmitScorefor 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 ascoreevent to an embedding page. CallgameOver(score)too if the host page also listens forgameover.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): GameSDKFires 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 |
|---|---|---|
| 8 s |
|
| 8 s |
|
| 6 s |
|
| 6 s |
|
| 6 s |
|
| 6 s |
|