XP, achievements & social
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
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 |
| Up to 100 XP per call, and lower caps per session and per day apply. Guests get 0. |
Reward a specific feat |
| 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 |
|
|
React to level-ups |
| The site also shows its own toast. |
Show the player's friends |
| Signed-in players only. |
Offer "Add friend" |
| Limited per play session and per hour. |
Link to a player's profile |
| Opens |
Draw an in-game high-score table |
|
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 afterstart(). 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:
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%.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 yourreasonstring as the subtitle. Players seereason, 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
grantedis 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 |
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 |
|---|---|---|---|
| string | yes | 2–48 characters, lowercase |
| string | yes | 1–60 characters. Shown on toasts, the game page and profiles. |
| string | yes | 1–200 characters. |
| string | no |
|
| string | no | Up to 24 characters. An emoji works best. It is shown as text, never as HTML. |
| boolean | no | If |
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 |
| A |
Publisher SDK | The |
MCP server | The |
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
gamificationblock (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
gamificationblock 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 callPOST /v1/games/:id/versionsdirectly, 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
achievementslist deactivates them all. Players who already earned them keep them on their profile, butunlockAchievementfor a deactivated key resolvesunlocked: falsewithreason: "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: trueand 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 |
|
Already had it |
|
Nothing unlocked |
|
reason says why nothing was unlocked:
| Cause |
|---|---|
| The player is a guest. |
| No play session yet: your game hasn't called |
| The session is under 30 seconds old or has fewer than 2 heartbeats. |
| The session has ended, or was flagged when it started. |
| The key isn't an active achievement of this game. |
| Over 60 unlock requests per minute. |
| No answer within 8 seconds. |
Another API error code, or | 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 |
|---|---|
| Rejected |
A grant that runs automatically on load, with no gameplay condition around it | Rejected |
A grant inside | Rejected |
A grant in a loop or | 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 |
|---|---|---|
|
| Every |
|
| XP from |
|
| An |
|
| A |
|
| An |
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/unlockAchievementcan 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'tawaitthem 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. -
reasonis shown to players in the XP toast. - When you send agamificationblock, 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: 0has many causes (guest, no session, too early, caps, rate limit, network). UsegetPlayer()to tell guests apart. Don't show "You earned 0 XP".Check
unlocked, notname, to tell a new unlock from a repeat.reasontells you why nothing unlocked.
Related
GameSDK progression & social reference: every method, parameter and return shape - GameSDK core reference: loading the SDK,
ready(),start(),on()In-game economy: what players can spend coins on - Packaging & publishing: uploading versions and the review process - Errors & limits