GameSDK progression & social reference

Embed
Preview of the card
<a href="https://coolgptgames.com/docs/gamesdk-progression-and-social"><img src="https://coolgptgames.com/docs/_api/public/orgs/062b11d9-2b86-4627-9e86-96816a67d7a1/spaces/07d97e67-936e-4223-9eaf-6998a46fee24/pages/25a91ead-0920-40a1-8c72-6610a320324f/social-card.svg?v=3e7ae33eec8b" alt="GameSDK progression &amp; social reference" width="600" height="315"></a>

This is the reference for the GameSDK methods that feed player progression (XP, levels, achievements) and social features (friends, profiles, the in-game leaderboard). For concepts, caps, how to declare achievements, and what players see, read XP, achievements & social first. For loading the SDK, ready(), start() and on(), see GameSDK core reference.

Common behaviour

These rules apply to every method on this page.

  • Runtimes. Available wherever window.GameSDK is loaded: HTML5 games that include the SDK script, and the cart runtimes (pico8, twine, bitsy), where it's added to the served page for you (see How games run). Calls work only while your game is running inside the Cool GPT Games player. Opened any other way, the page doesn't answer, so every method resolves its timeout fallback. - Signed-in vs guest. The player's sign-in state decides what happens. Guests always get the "guest" result listed for each method. Games embedded on other websites run as guests (see Embedding & the score bridge). - Play session. awardXp and unlockAchievement count only inside a live play session that has run for at least 30 seconds with at least 2 heartbeats. The session opens when your game calls GameSDK.start() (or GameSDK.replay.ready()). Without one, they resolve with the "nothing landed" result.

  • Never rejects, always settles. No method on this page rejects its promise or throws, and every promise has a built-in timeout. Failures come back as resolved values: zeros, empty arrays, unlocked: false with a reason, status: "error".

  • Message budget. The player page accepts at most 30 SDK messages per second from your game, across all methods. Extra messages are silently dropped, and a dropped request resolves its timeout fallback. - Replies are matched by request id. Each call gets the answer to its own request, so parallel and repeated calls are safe.

Method summary

Method

Resolves

Needs sign-in

Timeout / fallback

awardXp(amount, reason?)

{ granted, total, level, leveledUp }

Yes (guests get 0)

8 s → { granted: 0, total: 0, level: <last known>, leveledUp: false }

unlockAchievement(key)

{ key, name, alreadyHad, unlocked, reason? }

Yes (guests: reason: "sign_in_required")

8 s → unlocked: false, reason: "timeout"

getPlayer()

{ signedIn, level, handle }

No

5 s → last known player info

on(event, callback)

returns GameSDK

No

n/a

social.getFriends()

Array<{ handle, displayName, avatarUrl, level }> (up to 500)

Yes (guests get [])

6 s → []

social.addFriend(handle)

{ handle, status }

Yes (guests get "error")

6 s → { handle, status: "error" }

social.viewProfile(handle)

undefined (no promise)

No

n/a

social.getLeaderboard(scope?, opts?)

Array<{ rank, handle, displayName, avatarUrl, level, score }>

Only for "friends"

6 s → []


GameSDK.awardXp(amount, reason?)

awardXp(amount: number, reason?: string): Promise<{
  granted: number;
  total: number;
  level: number;
  leveledUp: boolean;
}>

Asks the platform to give the signed-in player XP for genuine gameplay. The platform decides how much actually lands.

Parameters

Name

Type

Required

Constraints

amount

number

yes

Converted with Number(amount), so non-numeric values become 0. Rounded down. <= 0 grants nothing. Values above 100 are cut to 100.

reason

string

no

Converted with String(). Up to 120 characters. Longer strings make the request fail (resolves as "nothing landed"). Shown to the player as the XP toast's subtitle. The first 100 characters are stored with the grant.

Resolved value

Field

Type

Meaning

granted

number

XP that actually landed. 0 … amount (after caps).

total

number

The player's lifetime XP after this grant.

level

number

The player's site-wide level after this grant.

leveledUp

boolean

true if this grant pushed the player up one or more levels.

Caps applied, in order: 100 per award → diminishing returns max(0.15, 1 − xpFromThisGameToday / 500) → 200 per game per session → 500 per game per player per UTC day → 3,000 per player per UTC day across the whole site. Achievement, first-play and personal-best XP from your game count toward the session and per-game daily totals. See XP, achievements & social for a worked example.

Guests and failures

Situation

Resolves

XP landed

{ granted: n, total, level, leveledUp }

All caps reached, or a non-positive amount

{ granted: 0, total: <real total>, level: <real level>, leveledUp: false }

Guest

{ granted: 0, total: 0, level: 1, leveledUp: false }, plus a one-time sign-in toast

No play session (start() not called)

{ granted: 0, total: 0, level: <last known>, leveledUp: false }

Session under 30 s / 2 heartbeats, ended, or flagged

{ granted: 0, total: 0, level: <last known>, leveledUp: false }

Rate-limited, invalid reason, or network/API error

{ granted: 0, total: 0, level: <last known>, leveledUp: false }

No answer within 8 s (for example, the request was dropped over 30 messages/s)

{ granted: 0, total: 0, level: <last known>, leveledUp: false }

<last known> is the player's level as last seen by the page or the SDK (from getPlayer() or an earlier awardXp answer), or 1 if it isn't known yet. In every "nothing landed" case except "caps reached", total is a placeholder 0, not the player's real total. Show total only when granted > 0. These placeholder answers never change the level the SDK remembers for getPlayer(). The SDK doesn't tell you why nothing landed.

Side effects

  • The xp event fires with the same object on every answer from the page, including granted: 0. It doesn't fire on the 8 s timeout.

  • If leveledUp, the levelUp event fires once with { level }.

  • The site shows a "+N XP" toast when granted > 0, and a "Level N!" toast on level-up. The player also gets a site notification and 100 bonus coins per level gained.

  • Every landed XP point also gives the player 1 coin (virtual currency).

Limits

  • 120 requests per minute per player per game. Over the limit, it resolves as "nothing landed".

  • The caps above.

  • Upload checks reject ungated grants on load or in setInterval, and flag grants in loops/timers and literal amounts of 10,000 or more for review.

Example

function onRoundWon(stars) {
  GameSDK.awardXp(10 + stars * 10, "Round won").then(function (r) {
    if (r.granted > 0) hud.flash("+" + r.granted + " XP");
    if (r.leveledUp) hud.flash("Level " + r.level + "!");
  });
}

Pitfalls

  • It can take up to 8 s to resolve if the page doesn't answer. Don't block gameplay on it.

  • Grants in the first ~30 s after start() return 0.

  • reason is visible to players, and over 120 characters the grant fails.


GameSDK.unlockAchievement(key)

unlockAchievement(key: string): Promise<{
  key: string;
  name: string | undefined;
  alreadyHad: boolean;
  unlocked: boolean;
  reason?: string;
}>

Unlocks one of your game's declared achievements for the signed-in player. It's safe to repeat: a player holds each achievement once. The XP reward is set by the achievement's tier (common 10, uncommon 25, rare 50, epic 100, legendary 200) and goes through the same caps as awardXp, including the 100-per-grant limit.

Parameters

Name

Type

Required

Constraints

key

string

yes

Converted with String(). Must match an active key declared for your game: 2–48 characters matching ^[a-z0-9][a-z0-9_]*$. Keys are case-sensitive.

Achievements are declared in a version's gamification block (sent with the upload-init or versions request, or through the CLI, publisher SDK or MCP server) and become active when that version is approved. See XP, achievements & social.

Resolved value

Field

Type

Meaning

key

string

The key you passed.

name

string | undefined

The achievement's display name. Set when the key matched an active achievement for a signed-in player in a valid session.

alreadyHad

boolean

true if the player had already unlocked it before this call.

unlocked

boolean

true only when this call newly unlocked it.

reason

string (optional)

Present only when neither unlocked nor alreadyHad is true: why nothing was unlocked.

Outcomes

Situation

Resolves

Toast

New unlock

{ key, name, alreadyHad: false, unlocked: true }

Achievement toast (plus level-up toast if one happened)

Already unlocked

{ key, name, alreadyHad: true, unlocked: false }

None

Unknown or deactivated key

{ key, name: undefined, alreadyHad: false, unlocked: false, reason: "unknown_achievement" }

None

Guest

unlocked: false, reason: "sign_in_required"

One-time sign-in toast

No play session (start() not called)

unlocked: false, reason: "no_session"

None

Session under 30 s / 2 heartbeats

unlocked: false, reason: "insufficient_play"

None

Session ended or flagged

unlocked: false, reason: "session_ended" or "session_flagged"

None

Rate-limited

unlocked: false, reason: "rate_limited"

None

Other API or network error

unlocked: false, reason: the API error code, or "error"

None

No answer within 8 s (for example, the request was dropped over 30 messages/s)

{ key, name: undefined, alreadyHad: false, unlocked: false, reason: "timeout" }

None

A new unlock is unlocked === true. Unlocking doesn't return the XP granted, but the levelUp event fires if the achievement's XP caused a level-up.

XP paid. An achievement pays its tier's XP in full (legendary = 200), skipping the 100-per-grant cap, the diminishing-returns curve and the per-session cap that apply to awardXp. It is still bounded by your game's 500 XP per player per UTC day and the player's 3,000 XP per day across the site, so a late unlock on a heavy day can pay less, or nothing. The player keeps the achievement regardless. To read what an unlock actually paid, use GET /v1/games/:slug/achievements/mine, whose xpGranted is the ledgered amount.

Side effects

  • The achievement event fires only when unlocked is true.

  • The unlock appears on the player's account page and in their public-profile showcase, and counts toward the site's daily "earn an achievement" quests.

Limits

  • 60 requests per minute per player per game.

  • At most 30 achievements per game.

  • A literal key in your code that the uploaded version doesn't declare gets the version rejected automatically. A version uploaded without a gamification block keeps the game's current achievements, so their keys count as declared.

Example

function onLevelComplete(level, hitsTaken, timeSec) {
  if (level === 1) GameSDK.unlockAchievement("first_clear");
  if (hitsTaken === 0) {
    GameSDK.unlockAchievement("untouchable").then(function (r) {
      if (r.unlocked) showBadgePopup(r.name);
    });
  }
  if (timeSec < 60) GameSDK.unlockAchievement("speedrunner");
}

Pitfalls

  • It can take up to 8 s to resolve if the page doesn't answer. Don't block gameplay on it.

  • A version approved with a gamification block replaces the game's achievement list: keys it leaves out are deactivated, and an empty achievements list deactivates them all. Unlocking a deactivated key resolves reason: "unknown_achievement". A version without a block leaves the list unchanged.

  • name can be set when nothing unlocked (the player already had it). Check unlocked to decide whether to celebrate.


GameSDK.getPlayer()

getPlayer(): Promise<{
  signedIn: boolean;
  level: number;
  handle: string | null;
}>

Returns who is playing: whether they're signed in, their site-wide level, and their handle.

Parameters

None.

Resolved value

Field

Type

Meaning

signedIn

boolean

Whether the player is signed in to Cool GPT Games.

level

number

Site-wide level (1–999). Always 1 for guests.

handle

string | null

The player's handle (without @), or null for guests.

Situation

Resolves

Signed in

{ signedIn: true, level: 7, handle: "alice" }

Guest

{ signedIn: false, level: 1, handle: null }

Signed in, but looking up progress or profile failed

{ signedIn: true, level: 1, handle: null } (either field can fall back separately)

No answer within 5 s

The SDK's last known player info (see pitfalls)

Side effects

The player event fires with the same object when the answer arrives.

Limits

None beyond the 30-messages-per-second budget. The page looks the player up once (two API requests) and then answers from memory. Parallel calls share that one lookup, and a failed lookup is retried on the next call.

Example

GameSDK.getPlayer().then(function (p) {
  if (p.signedIn) {
    title.textContent = "Welcome back" + (p.handle ? ", @" + p.handle : "") + "!";
    levelBadge.textContent = "Lv " + p.level;
  } else {
    title.textContent = "Playing as guest: sign in to earn XP";
  }
});

Pitfalls

  • The 5 s fallback returns the SDK's last known info. Before any answer, that's the guest shape. Don't treat a fallback as definitive.

  • No display name, avatar, XP or title is exposed. The remembered level is kept current by awardXp answers and level-ups, but call again (or listen to levelUp) if you display it.


GameSDK.on(event, callback)

on(event: string, callback: (payload: any) => void): typeof GameSDK

Subscribes to SDK events. Chainable. Exceptions thrown in your callback are caught and ignored. Events for this area:

Event

Payload

Fires

xp

{ granted, total, level, leveledUp }

After every awardXp answer from the page (including granted: 0). Not fired on the 8 s timeout.

levelUp

{ level }

Once per level, when awardXp or an achievement's XP causes a level-up.

achievement

{ key, name, alreadyHad: false, unlocked: true }

Only when an unlockAchievement call newly unlocked the achievement.

player

{ signedIn, level, handle }

When a getPlayer() answer arrives.

friendAdded

{ handle, status }

Only when addFriend succeeded (status is "outgoing" or "friends").

GameSDK
  .on("levelUp", function (e) { confetti(); showLevel(e.level); })
  .on("achievement", function (a) { log("Unlocked " + a.name); });

The full event list (pause, resume, ads and more) is in GameSDK core reference.


GameSDK.social.getFriends()

social.getFriends(): Promise<Array<{
  handle: string;
  displayName: string | null;
  avatarUrl: string | null;
  level: number;
}>>

Returns the signed-in player's accepted friends, most recently befriended first, up to 500.

Parameters

None.

Resolved value

An array of friend objects:

Field

Type

Meaning

handle

string

Friend's handle. Use it with viewProfile.

displayName

string | null

Friend's display name, if set.

avatarUrl

string | null

URL of the friend's avatar image, if any.

level

number

Friend's site-wide level.

Pending requests (incoming or outgoing) aren't included.

Situation

Resolves

Signed in

Array (may be empty)

Guest

[]

Network/API error

[]

No answer within 6 s

[]

Limits

The call returns at most 500 friends. A player may hold up to 1,000, so treat this as their most recent friends rather than the complete list. There's no paging and no total count through the SDK; read the site's own friends endpoint for those. Otherwise, nothing beyond the 30-messages-per-second budget.

Example

GameSDK.social.getFriends().then(function (friends) {
  friendsPanel.innerHTML = "";
  friends.forEach(function (f) {
    var li = document.createElement("li");
    li.textContent = (f.displayName || "@" + f.handle) + " · Lv " + f.level;
    li.onclick = function () { GameSDK.social.viewProfile(f.handle); };
    friendsPanel.appendChild(li);
  });
});

Pitfalls

  • An empty array can mean "guest", "no friends" or "error". Use getPlayer() to show the right empty state.

  • A player at the 500-row ceiling has friends you can't see here. Don't present the list as complete, and don't infer "not a friend" from an absent handle.

  • Display names come from players. Insert them with textContent, never innerHTML.


GameSDK.social.addFriend(handle)

social.addFriend(handle: string): Promise<{
  handle: string;
  status: "outgoing" | "friends" | "self" | "error";
}>

Sends a friend request from the signed-in player to handle. If handle had already sent this player a request, the two become friends immediately.

Parameters

Name

Type

Required

Constraints

handle

string

yes

Converted with String(). 1–40 characters of A–Z a–z 0–9 _ - (no @). Case-insensitive match.

Resolved value

Field

Type

Meaning

handle

string

The handle you passed.

status

string

One of the values below.

status

Meaning

"outgoing"

A request is pending from this player to handle, whether just sent or sent earlier.

"friends"

They're friends: already, or just now because a reciprocal request was accepted.

"self"

handle is the signed-in player's own handle. Nothing happened.

"error"

Nothing happened. See causes below.

Causes of "error": guest, invalid handle format, no such player, either player has blocked the other, over the per-load or hourly limit, either player is at the 1,000-friend ceiling, this player already has 100 outgoing requests open, network/API error, or no answer within 6 s.

The SDK collapses all of these into "error", so your game can't tell them apart. Over REST the account ceilings are distinguishable: 409 friend_limit and 409 friend_request_limit.

Side effects

  • On "outgoing", the other player gets a site notification (and possibly an email): "@you sent you a friend request".

  • On a reciprocal accept, the original requester gets "@you accepted your friend request".

  • The site shows the player a toast: "Friend request sent to @handle" or "You're now friends with @handle".

  • Guests see the one-time sign-in toast.

  • The friendAdded event fires with the result only for "outgoing" and "friends".

Limits

Limit

Value

Past the limit

Per game load

8 requests (counted even when a request fails or targets yourself; invalid handles and guest calls aren't counted)

"error" until the player page is reloaded

Per player, site-wide

30 per hour

"error"

Friends per account

1,000, checked for both players

"error" (friend_limit over REST)

Open outgoing requests

100 at once

"error" (friend_request_limit over REST)

Example

addFriendBtn.onclick = function () {
  addFriendBtn.disabled = true;
  GameSDK.social.addFriend(row.handle).then(function (r) {
    addFriendBtn.textContent =
      r.status === "friends" ? "Friends ✓" :
      r.status === "outgoing" ? "Requested" :
      r.status === "self" ? "That's you" : "Try later";
    addFriendBtn.disabled = r.status !== "error";
  });
};

Pitfalls

  • Call it only after the player explicitly asks. Never auto-friend.

  • Take handles from getLeaderboard() / getFriends(). Typed handles often fail.

  • The 8-per-load budget is shared by all your calls, including failed ones.

  • Since "error" also covers a permanent refusal (a full friends list, too many open requests), don't word your failure message as "try again later". "Couldn't send" is safer, and don't retry automatically.


GameSDK.social.viewProfile(handle)

social.viewProfile(handle: string): void

Opens the public profile of handle (https://coolgptgames.com/u/<handle>) in a new browser tab. The profile shows level ring and title, XP, achievement count, achievement showcase, friends, published games, and add-friend / follow buttons.

Parameters

Name

Type

Required

Constraints

handle

string

yes

1–40 characters of A–Z a–z 0–9 _ -. Anything else is silently ignored.

Return value

undefined. There's no promise and no confirmation. If the handle doesn't exist, the tab shows a not-found profile.

Guests and limits

Works for guests. Counts toward the 30-messages-per-second budget.

Example

leaderboardRow.addEventListener("click", function () {
  GameSDK.social.viewProfile(entry.handle);
});

Pitfalls

  • Call it synchronously inside a real input handler (click, tap or key). The user gesture propagates from the game's sandboxed cross-origin frame to the page, and the tab opens in Chromium, Firefox and WebKit.

  • The gesture expires after about 5 seconds. A call made after an await on a network request is blocked as a popup — measured failing at 6 s. Resolve any handle you need (for example from getLeaderboard()) before the click, then call viewProfile() straight from the handler.

  • A blocked call is silent. viewProfile() returns undefined either way and there is no event or error to catch, so you can't detect or retry it.

  • Don't call it on load, on a timer, or after your own animation or confirmation step — those spend the same 5-second budget.


GameSDK.social.getLeaderboard(scope?, opts?)

social.getLeaderboard(scope?: "global" | "friends", opts?: { verified?: boolean }): Promise<Array<{
  rank: number;
  handle: string;
  displayName: string | null;
  avatarUrl: string | null;
  level: number;
  score: number;
}>>

Returns your game's top scores so you can draw an in-game leaderboard with profile and add-friend actions. This page covers the social side. For how scores are submitted, validated and ranked, see Scores, leaderboards & anti-cheat.

Parameters

Name

Type

Required

Constraints

scope

string

no

"friends" ranks the signed-in player and their friends only. Any other value, or none, means "global".

opts.verified

boolean

no

true returns the replay-verified board — only scores the platform re-simulated and reproduced exactly — ranked by the re-simulated best. Default false (the public board).

Resolved value

Up to 50 rows, highest score first, each player's all-time best on the board you asked for. avatarUrl is included even though some older examples leave it out.

rank is a competition rank: tied players share it and the next rank skips (1, 2, 2, 4), with the earliest-reached score listed first within a tie. getMyScore() ranks the same way, so the two agree. Banned and deleted players never appear and never count toward a rank.

Situation

Resolves

"global"

Array (anyone, including guests)

"friends", signed in

Array of the player plus friends who have a score

"friends", guest

[]

{ verified: true }, no verified scores yet

[]

Network/API error, or no answer within 6 s

[]

Weekly and monthly boards aren't available through this method. See Scores, leaderboards & anti-cheat.

Don't use rank as a unique key or list index: two rows can share it.

Example

GameSDK.social.getLeaderboard("friends").then(function (rows) {
  rows.forEach(function (r) {
    addRow("#" + r.rank, r.displayName || "@" + r.handle, r.score, {
      onProfile: function () { GameSDK.social.viewProfile(r.handle); },
      onAdd: function () { GameSDK.social.addFriend(r.handle); },
    });
  });
});

Updated