Guides

XP, achievements & social

AdminUpdated Sep 22, 2026

XP, achievements & social

Cool GPT Games has one player progression system shared by every game on the site. Players earn XP, go up levels, unlock titles, and collect achievements that appear on their public profile. Your game can feed that system and build social features on top of it: in-game friend lists, "add friend" buttons and links to player profiles.

This guide covers how the system works and how to use it well. For exact signatures and return shapes, see GameSDK progression & social reference.

The platform works on one rule: your game proposes, the platform decides. You choose when to grant XP or unlock an achievement. The platform decides how much actually lands. It caps every grant, grants only during genuine play sessions, and checks your code when you upload. So award XP for real gameplay and let the caps do the policing.

Coins mentioned on this page are the site's virtual currency. They are never real money and can't be cashed out.

At a glance

You want to…

Use

Notes

Reward a win, milestone or level clear

GameSDK.awardXp(amount, reason)

Up to 100 XP per call, and lower caps per session and per day apply. Guests get 0.

Reward a specific feat

GameSDK.unlockAchievement(key)

The key must be declared when you upload. The tier sets the XP. Safe to call more than once.

Greet the player / show their level

GameSDK.getPlayer()

{ signedIn, level, handle }

React to level-ups

GameSDK.on("levelUp", cb)

The site also shows its own toast.

Show the player's friends

GameSDK.social.getFriends()

Signed-in players only.

Offer "Add friend"

GameSDK.social.addFriend(handle)

Limited per play session and per hour.

Link to a player's profile

GameSDK.social.viewProfile(handle)

Opens /u/<handle> in a new tab.

Draw an in-game high-score table

GameSDK.social.getLeaderboard(scope)

See Scores, leaderboards & anti-cheat.

All of these go through the GameSDK bridge. Your game runs in a sandboxed frame, and the Cool GPT Games page holding it makes the real API calls with the player's session. Your game never sees a token. For how to load the SDK and how the bridge works, see How games run and GameSDK core reference.

Before anything counts: signed-in players and play sessions

Progression is tied to an account, so two conditions must hold before XP or an achievement is recorded.

1. The player is signed in. Guests can play your game normally, but awardXp resolves with granted: 0 and unlockAchievement resolves unlocked: false with reason: "sign_in_required". The first time a guest triggers either call, the site shows a one-time toast: "Sign in to keep your XP & achievements. You're earning as a guest." Nothing a guest earns is kept for later. Games embedded on other websites run as guests (see Embedding & the score bridge).

2. A play session is live and has enough real play. The site opens a play session the first time your game calls GameSDK.start() (or GameSDK.replay.ready()), then sends a heartbeat every 15 seconds. XP and achievement requests count only when the session:

  • has been open for at least 30 seconds, and - has received at least 2 heartbeats, and

  • is still open and hasn't been flagged as invalid.

Until then the request returns nothing (granted: 0, or unlocked: false with a reason such as "no_session" or "insufficient_play"). In practice:

  • Call GameSDK.start() when gameplay begins. If your game never calls it, no session exists and every grant returns 0. - Don't grant in the first ~30 seconds after start(). A "first move" reward will almost always be lost. Wait for a real milestone.

  • Leaderboard scores need about a minute of play (see Scores, leaderboards & anti-cheat).

GameSDK.ready();                        // assets loaded
startButton.onclick = function () {
  GameSDK.start();                      // opens the play session
  beginRound();
};

XP: awardXp(amount, reason)

function onLevelCleared(levelNumber, noDamage) {
  GameSDK.awardXp(noDamage ? 40 : 25, "level_clear").then(function (r) {
    // r = { granted, total, level, leveledUp }
    if (r.granted > 0) showFloatingText("+" + r.granted + " XP");
  });
}

amount is a request. The platform works out what actually lands:

  1. Non-positive or non-numeric amounts grant nothing. Fractions are rounded down. 2. Per-award cap: 100 XP. Larger requests are cut to 100. 3. Diminishing returns. As your game's XP for this player today approaches its daily cap, each request is scaled down by max(0.15, 1 − xpFromThisGameToday / 500). Early grants land at full value and later ones shrink, but never below 15%.

  2. Per-session cap: 200 XP from your game in one play session. 5. Per-game daily cap: 500 XP from your game to one player per UTC day. 6. Global daily cap: 3,000 XP per player per UTC day, across all games and platform bonuses.

Achievement XP and personal-best XP from your game count toward the same per-session and per-game daily totals (see below).

Worked example. A fresh player on a new UTC day. Your game calls awardXp(150) → cut to 100, 100 lands. Then awardXp(80): 100 of today's 500 used, so the factor is 0.8 → 64 lands. Then awardXp(80) again: session total is 164, so only 36 of the 200-per-session room is left → 36 lands. Further calls in this session land 0.

Rate limit. Beyond the caps, XP requests are limited to 120 per minute per player per game. Over the limit, the call resolves with granted: 0.

What the player sees

  • When XP lands (granted > 0), the site shows a toast: "+25 XP", with your reason string as the subtitle. Players see reason, so make it readable ("Level clear", "Boss defeated"), not a debug code.

  • On a level-up, a second toast shows "Level 8! Level up". The player also gets a site notification ("You reached level 8! 🎉") with bonus coins.

  • Nothing is shown when granted is 0.

You don't need to draw your own XP UI, but you can. The resolved value includes the player's new total and level.

How XP turns into levels and titles

XP adds up for life across every game. Going from level L to L + 1 costs round(100 × L^1.5) XP, so early levels come fast and later ones take longer. The maximum level is 999.

Level

Total XP needed

Title unlocked

Profile showcase slots

1

0

Newcomer

3

2

100

3

383

5

1,703

Regular

4

10

11,106

Enthusiast

5

15

31,998

Sharpshooter

6

25

118,809

Veteran

8

40

392,197

Master

10

60

1,092,274

Grandmaster

12

100

3,950,120

Legend

16

Each title tier also comes with an avatar ring colour. Players see their title, ring, level and XP bar on their profile and account pages.

Coins ride along with XP. Every XP point that lands also gives the player 1 coin, plus 100 bonus coins for each level gained. Players can spend those coins in your store (see In-game economy).

Other XP your game earns for players

You don't have to call anything for these. The platform grants them itself:

Source

XP

When

First play of a game

20

The first valid session of your game for that player. Counts toward your game's caps.

New personal best

15

A submitScore that beats the player's best (see Scores, leaderboards & anti-cheat). Counts toward your game's caps.

Achievement unlock

By tier

See below. Counts toward your game's caps.

Daily play streak

15 × streak days, max 105

Once per UTC day, when a valid session ends. Counts only toward the global cap.

Daily quests

Varies

Site-wide quests like "play 3 games" or "earn an achievement". Count only toward the global cap.

Because first-play, personal-best and achievement XP use up your game's session and daily room, a session with several unlocks and a personal best may leave less room for awardXp than you expect.

Season pass

The site runs seasons. A player's season XP is every XP point they earned since the current season started, from any game or source. Players claim season-track rewards (coins and cosmetic badges or frames) on the site. There is no SDK call for seasons. Your game helps a player's season progress simply by granting XP.

Achievements: unlockAchievement(key)

Achievements are specific feats your game declares up front: "First Win", "Untouchable", "Speedrunner". Each one has a tier, and the tier fixes how much XP it is worth. You never set achievement XP yourself.

1. Declare them when you upload a version

Achievements are declared per game version, in a gamification block. At most 30 per game.

{
  "gamification": {
    "achievements": [
      { "key": "first_win",  "name": "First Win",  "description": "Win your first round",        "tier": "common", "icon": "🏆" },
      { "key": "no_damage",  "name": "Untouchable", "description": "Clear a level without a hit", "tier": "rare",   "icon": "🛡️" },
      { "key": "true_ending", "name": "???",         "description": "Find the true ending",        "tier": "epic",   "secret": true }
    ],
    "xp": { "note": "XP on level clear (25-40) and boss kills (50).", "leaderboard": true }
  }
}

Field

Type

Required

Rules

key

string

yes

2–48 characters, lowercase snake_case: must match ^[a-z0-9][a-z0-9_]*$. Unique within the game. This is what you pass to unlockAchievement.

name

string

yes

1–60 characters. Shown on toasts, the game page and profiles.

description

string

yes

1–200 characters.

tier

string

no

common (default), uncommon, rare, epic or legendary.

icon

string

no

Up to 24 characters. An emoji works best. It is shown as text, never as HTML.

secret

boolean

no

If true, the game page shows it as "???" / "Hidden achievement" until the player unlocks it.

The optional xp object is a note for reviewers: note (up to 500 characters) describes how your game grants XP, and leaderboard: true says it reports scores. It isn't enforced. It helps your game get through review.

Tier XP:

Tier

XP

common

10

uncommon

25

rare

50

epic

100

legendary

200

Achievement XP goes through the same caps as awardXp: the 100-per-grant limit, diminishing returns, the session cap and the per-game daily cap. In practice a legendary unlock lands at most 100 XP, and an unlock late in a busy session may land less than its tier value. Players always keep the achievement itself, even if its XP was capped.

How to send the block. Every publishing path except the web upload form can send it:

Tool

Where the block goes

REST API

In the body of POST /v1/upload/init (alongside gameId, runtime and filename) or POST /v1/games/:id/versions (alongside runtime, and optionally changelog)

arcadey CLI

A gamification block in your game folder's game.json. arcadey publish sends it with the version.

Publisher SDK

The gamification option of publish(...), uploadVersion(...) or publishVersion(...)

MCP server

The gamification argument of publish_game or upload_version

If the block doesn't match the rules above, the request fails with a validation error. See Packaging & publishing and REST API overview for the full upload flow.

curl -X POST "$API/v1/upload/init" \
  -H "Authorization: Bearer $COOLGPTGAMES_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "gameId": "YOUR_GAME_ID",
    "runtime": "html5",
    "filename": "bundle.zip",
    "gamification": {
      "achievements": [
        { "key": "first_win", "name": "First Win", "description": "Win your first round", "tier": "common", "icon": "🏆" }
      ]
    }
  }'

Uploading without a block keeps your achievements. A version uploaded with no gamification block (for example, through the web upload form) keeps the game's current achievements unchanged. Their keys count as declared for that version, so code that unlocks them passes the upload check.

The CLI, Publisher SDK and MCP server send a version's release notes through a request that can't be combined with a gamification block yet. If you pass both a changelog and a block, the tool stops with an error before uploading. Drop the changelog for versions that change your achievements, or call POST /v1/games/:id/versions directly, which accepts both.

If your code calls unlockAchievement("some_key") with a literal key that is neither in the version's block nor (for a version without a block) among the game's current achievements, the upload check treats it as an attempt to fake achievements, and the version is rejected automatically.

2. What happens when a version is published

Declared achievements join the game's catalog when the version is approved, not when it's uploaded. An approved version with a gamification block brings the catalog in line with that block:

  • New keys are added.

  • Existing keys are updated, including name, description, icon, secret, and tier (which changes the XP for future unlocks).

  • Keys missing from the block are deactivated, and an empty achievements list deactivates them all. Players who already earned them keep them on their profile, but unlockAchievement for a deactivated key resolves unlocked: false with reason: "unknown_achievement".

An approved version without a block leaves the catalog exactly as it was.

So whenever you send a block, declare the full list, not just the new ones.

3. Unlock from your game

function onBossDefeated(tookDamage) {
  GameSDK.unlockAchievement("first_win");
  if (!tookDamage) {
    GameSDK.unlockAchievement("no_damage").then(function (r) {
      // r = { key, name, alreadyHad, unlocked, reason? }
    });
  }
}
  • Safe to repeat. A player can hold each achievement once. Later calls resolve with alreadyHad: true and grant nothing extra.

  • Unknown or inactive keys unlock nothing and resolve reason: "unknown_achievement".

  • Rate limit: 60 unlock requests per minute per player per game.

  • The same signed-in and play-session conditions as XP apply.

What the player sees. On a first unlock, a toast shows the achievement name with "🏆 Achievement" (or your icon), plus a level-up toast if the XP caused one. The achievement then appears in:

  • the player's account page (/me), listed newest first;

  • the Showcase on their public profile (/u/<handle>), which shows their most recent unlocks up to their level's showcase-slot count.

Your game's page on the site also has an Achievements section listing every active achievement with its icon, tier and tier XP. Secret ones show as "???" until unlocked.

Telling a real unlock from a no-op

unlockAchievement always resolves (within 8 seconds at most), and tells you exactly what happened:

Result

Resolved value

New unlock

{ key, name: "First Win", alreadyHad: false, unlocked: true }

Already had it

{ key, name: "First Win", alreadyHad: true, unlocked: false }

Nothing unlocked

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

reason says why nothing was unlocked:

reason

Cause

"sign_in_required"

The player is a guest.

"no_session"

No play session yet: your game hasn't called start() (or replay.ready()).

"insufficient_play"

The session is under 30 seconds old or has fewer than 2 heartbeats.

"session_ended", "session_flagged"

The session has ended, or was flagged when it started.

"unknown_achievement"

The key isn't an active achievement of this game.

"rate_limited"

Over 60 unlock requests per minute.

"timeout"

No answer within 8 seconds.

Another API error code, or "error"

The request failed.

The achievement event fires only for a new unlock.

GameSDK.unlockAchievement("no_damage").then(function (r) {
  if (r.unlocked) playFanfare();
  else if (r.reason === "unknown_achievement") console.warn("Not declared:", r.key);
});

Upload checks that protect the economy

When you upload a version, an automatic check reads your game's JavaScript and looks for ways XP or achievements could be earned without real play:

Finding

Effect

unlockAchievement("literal_key") with a key this version doesn't declare (a version without a block counts the game's current achievements as declared)

Rejected

A grant that runs automatically on load, with no gameplay condition around it

Rejected

A grant inside setInterval with no gameplay condition

Rejected

A grant in a loop or setTimeout with no condition

Sent to human review

A grant in a load or timer callback that is wrapped in a condition

Sent to human review

A literal XP amount of 10,000 or more

Sent to human review

More than 5 epic/legendary achievements declared

Sent to human review

Code the check can't parse

Sent to human review

To pass the check, put every grant inside the code that handles a real gameplay outcome (if (won) …, onLevelComplete()), declare every key you unlock, and keep amounts modest. Rejection reasons show in your game's moderation status (see Packaging & publishing).

Reading the player: getPlayer()

GameSDK.getPlayer().then(function (p) {
  // signed in: { signedIn: true, level: 7, handle: "alice" }
  // guest:     { signedIn: false, level: 1, handle: null }
  hud.name.textContent = p.signedIn ? "@" + p.handle : "Guest";
  hud.level.textContent = "Lv " + p.level;
  if (!p.signedIn) showSignInHint();
});

The result has only these three fields. There is no display name, avatar, XP total or title. handle can also be null for a signed-in player if the profile lookup failed, so always handle a missing value. The page looks the player up once and then answers from memory, so calling it again (for example, when returning to your menu) is cheap. See the reference for timeout behaviour.

Events

Subscribe with GameSDK.on(event, callback):

Event

Payload

Fires when

xp

{ granted, total, level, leveledUp }

Every awardXp answer from the page, including granted: 0.

levelUp

{ level }

XP from awardXp or an achievement caused a level-up. Fires once per level.

achievement

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

An unlockAchievement call newly unlocked the achievement.

player

{ signedIn, level, handle }

A getPlayer() answer arrives.

friendAdded

{ handle, status }

An addFriend call succeeded ("outgoing" or "friends").

GameSDK.on("levelUp", function (e) { celebrate(e.level); });
GameSDK.on("achievement", function (a) { showBadge(a.name); });

Social: friends and profiles

Players have a friends list on Cool GPT Games. Your game can read it, send friend requests for the player, and open profiles. The site handles sign-in, notifications and the friend-request inbox.

Showing friends

GameSDK.social.getFriends().then(function (friends) {
  // [{ handle, displayName, avatarUrl, level }, …]
  if (!friends.length) return showEmptyState("Add friends to compete with them!");
  friends.forEach(function (f) {
    addRow(f.displayName || "@" + f.handle, "Lv " + f.level, f.avatarUrl);
  });
});

The list includes accepted friends only, most recently added first. Pending requests aren't included. Guests, players with no friends, and failed lookups all get [].

Adding a friend

function onAddFriendClicked(handle) {
  GameSDK.social.addFriend(handle).then(function (r) {
    if (r.status === "outgoing") setButton(handle, "Request sent");
    else if (r.status === "friends") setButton(handle, "Friends ✓");
    else if (r.status === "self") setButton(handle, "That's you");
    else setButton(handle, "Couldn't send");   // "error"
  });
}
  • Sends a friend request as the signed-in player. If the other player had already sent this player a request, the two become friends straight away ("friends").

  • The other player gets a site notification (and possibly an email) saying "@alice sent you a friend request".

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

  • Limits: your game can send 8 friend requests per game load, and the player can send 30 per hour across the whole site. Past either limit, calls resolve with status: "error".

  • Guests get "error" and see the one-time sign-in toast. Your own handle resolves "self".

Only offer "Add friend" after the player chooses it, for example a button next to a leaderboard row. Take handles from getLeaderboard() or getFriends() rather than asking the player to type one.

Opening a profile

row.onclick = function () { GameSDK.social.viewProfile(entry.handle); };

This opens https://coolgptgames.com/u/<handle> in a new tab. The profile shows the player's level ring and title, total XP, achievement count, achievement showcase, friends, games they've published, and buttons to add them as a friend or follow them. Call it straight from a click or tap handler: browsers may block new tabs that aren't opened in response to user input.

Leaderboards in social UI

GameSDK.social.getLeaderboard(scope) returns your game's top 50 all-time scores as [{ rank, handle, displayName, avatarUrl, level, score }]. scope is "global" (default) or "friends", which ranks the player and their friends only. A guest asking for "friends" gets []. It's the natural source of handles for "Add friend" and "View profile" buttons. For score submission, verified boards, weekly/monthly boards and anti-cheat, see Scores, leaderboards & anti-cheat.

Following creators

Players can also follow creators from their profile or game pages. Followers see a creator's new games in their feed. Following happens on the site only. There is no GameSDK call for it.

Your creator progression

Creators have a separate progression track, creator levels, which the in-game SDK doesn't affect. You earn creator XP for each published game (200 XP) and for creator achievements such as "First Launch", "Viral" and "Critically Acclaimed", based on plays, ratings and reviews. Higher creator levels unlock perks: more uploads per hour (5 at level 1, up to 50 at level 25), priority moderation at level 10 (approved games go straight to the public catalog), and a Featured Creator badge with boosted discovery at level 15. Your creator title and level appear on your profile. Creator XP is calculated from your published library, so there is nothing to call.

Pitfalls checklist

  • No GameSDK.start() → no session → no XP or achievements. Call it when gameplay begins. - Grants in the first ~30 seconds of a session are lost. Reward milestones, not the first move. - awardXp / unlockAchievement can take up to 8 seconds to resolve if a request never gets through (for example, you send more than 30 SDK messages in one second and the extras are dropped). Don't await them where they would block gameplay. Fire and forget, or use .then.

  • Don't spam requests. Grant once per real event. Extra requests only use up the message budget and the rate limits. - reason is shown to players in the XP toast. - When you send a gamification block, declare every achievement in it. Keys left out are deactivated when the version is approved. A version without a block keeps the current list. - Don't unlock undeclared keys. A literal undeclared key in your code gets the version rejected. - granted: 0 has many causes (guest, no session, too early, caps, rate limit, network). Use getPlayer() to tell guests apart. Don't show "You earned 0 XP".

  • Check unlocked, not name, to tell a new unlock from a repeat. reason tells you why nothing unlocked.

Related

Was this page helpful?