API Docs

GameSDK reference: ads, economy & tournaments

AdminUpdated Sep 22, 2026

GameSDK reference: ads, economy & tournaments

This page is the reference for these GameSDK calls:

Area

Calls

Ads

requestAd

Economy

economy.listItems, economy.purchase, economy.getEntitlements

Tournaments

tournaments.list, tournaments.getActive, tournaments.join, tournaments.getStandings

Events

adStarted, adComplete, adError, purchase, tournamentJoined

For concepts and walkthroughs, see Ads and rewarded ads, In-game economy and Tournaments. For loading the SDK, GameSDK.on, and the core lifecycle calls, see GameSDK core reference.

How these calls behave

All of these calls send a message from your sandboxed game to the Cool GPT Games page, which calls the API for you. Your game never handles the player's credentials. Keep these rules in mind:

  • No call rejects. Every method here returns a Promise that resolves, with an error object or null when something goes wrong. You don't need try/catch, but you do need to check the value.

  • Every call settles. Economy and tournament calls resolve null if the page doesn't answer within 6 seconds. requestAd resolves { error: "timeout" } after 60 seconds. - Message rate limit. The page accepts at most 30 SDK messages per second from your game, counting every SDK call. Extra messages are silently dropped, so the call then times out. - Sign-in comes from the site. "Signed in" means the player is signed in to Cool GPT Games in that browser. Your game can check with GameSDK.getPlayer() (see GameSDK core reference). - Only on Cool GPT Games. Outside the Cool GPT Games player, for example when you open your index.html locally, nobody answers. Economy and tournament calls resolve null after 6 seconds, and requestAd resolves { error: "timeout" } after 60 seconds. Guard for this in local development.


Ads

GameSDK.requestAd(placement)

GameSDK.requestAd(placement?: string): Promise<{ rewarded: boolean } | { error: string }>

Asks the page to show an ad over your game. The promise settles exactly once.

Parameters

Name

Type

Required

Notes

placement

string

No

"rewarded" requests a rewarded ad. Any other value, or none, requests a "midroll" (interstitial) ad. No other placement values exist for games.

Resolves

Value

When

{ rewarded: true }

A rewarded ad was watched to the end of its 5-second countdown, and the ad server confirmed the reward

{ rewarded: false }

A rewarded ad was closed early or the ad server didn't confirm the reward, or a midroll finished (midrolls never reward)

{ error: "no_fill" }

No ad was available

{ error: "no_consent" }

The visitor hasn't allowed ads (banner unanswered, rejected, or the consent tool couldn't load)

{ error: "no_session" }

Midroll only: requested before GameSDK.start() or GameSDK.replay.ready() began a play session

{ error: "busy" }

Another ad is still in progress. That earlier ad and its promise carry on unaffected.

{ error: "timeout" }

The page didn't answer within 60 seconds

{ error: "error" }

Network or other failure

Events fired

Case

Events

Ad shown

adStarted, then adComplete { rewarded }

no_fill, no_consent, no_session, error

adError { reason } only

busy

adError { reason: "busy" } only. The ad already running keeps its own events.

timeout

adError { reason: "timeout" }, fired by the SDK

Failure behaviour

  • The promise never rejects, and it settles exactly once: with the ad's result, or with one error.

  • 60-second timeout. If the page never answers (the message was dropped, or you're not running on Cool GPT Games), the promise resolves { error: "timeout" } and adError { reason: "timeout" } fires. If the page's answer arrives later anyway, its adStarted / adComplete / adError events still fire, but the promise has already settled.

  • One ad at a time. Calling requestAd while an earlier ad is in progress resolves the new call with { error: "busy" } straight away. The earlier call isn't affected and still settles with its own result. (An ad that has been in progress for more than 90 seconds no longer blocks a new one.)

Sign-in: not required. Guests get ads and rewarded results the same way as signed-in players.

Limits: ad requests and ad events are limited to 60 per minute per play session (or per game when there's no session). There's no enforced per-session cap or minimum interval between ads.

Example

async function watchForReward() {
  button.disabled = true;
  const r = await GameSDK.requestAd("rewarded");   // always settles, at most 60 s
  button.disabled = false;
  if (r.rewarded === true) grantBonus();
  else if (r.error === "no_consent") toast("Allow ads to earn this reward.");
}

Pitfalls

  • Grant rewards on rewarded === true only. adComplete also fires for closed, unconfirmed and midroll ads.

  • The platform doesn't pause your game. Pause on adStarted and resume on adComplete or adError, and make resuming safe to run twice. Note that adError { reason: "busy" } fires while another ad is still showing, so don't resume on busy. Disabling your ad button until the promise settles avoids it entirely.

  • The pause and resume events are about tab visibility, not ads.

  • The platform grants nothing for a rewarded ad. The reward is whatever your game gives.

  • Midroll ads may not fill at all. Never make progress depend on one.


Economy

Items are defined per game by the creator. See In-game economy. Prices are in coins, a virtual currency with no real-money value.

GameSDK.economy.listItems()

GameSDK.economy.listItems(): Promise<Item[] | null>

interface Item {
  sku: string;
  name: string;
  description: string | null;
  priceCoins: number;   // integer ≥ 0
}

Returns this game's active store items.

Parameters: none.

Resolves: an array of Item. The array order isn't guaranteed, so sort it yourself.

Failure behaviour: resolves [] if the API returns an error, and null on a network error or after the 6-second timeout.

Sign-in: not required. Guests see the same list.

Example

const items = (await GameSDK.economy.listItems()) || [];
items.sort(function (a, b) { return a.priceCoins - b.priceCoins; });
items.forEach(function (it) { addRow(it.name, it.priceCoins + " coins", it.sku); });

Pitfalls: inactive or deleted items aren't listed, but players may still own them. Always combine this list with getEntitlements().


GameSDK.economy.purchase(sku)

GameSDK.economy.purchase(sku: string): Promise<PurchaseResult | { error: string } | null>

type PurchaseResult =
  | { ok: true; purchased: string; coins: number }       // newly bought
  | { ok: true; alreadyOwned: true; coins: number };     // already owned, not charged

Buys one item for the signed-in player with their coins. On success the player owns the SKU in this game permanently.

Parameters

Name

Type

Required

Constraints

sku

string

Yes

Must match an active item on this game. 1–64 characters. The value is converted with String().

Resolves

Value

Meaning

{ ok: true, purchased: sku, coins }

Bought. coins is the player's balance after the purchase. For a 0-price item nothing is charged.

{ ok: true, alreadyOwned: true, coins }

Already owned. No charge. coins is the current balance.

{ error: "sign_in_required" }

The player is a guest. Nothing happened.

{ error: "insufficient_coins" }

The balance is too low. Nothing was charged.

{ error: "not_found" }

No active item with that SKU on this game, or the game wasn't found

{ error: "rate_limited" }

More than 60 purchases per minute by this player

{ error: "validation_error" }

The SKU was longer than 64 characters

{ error: "unauthorized" }

The player's sign-in is no longer valid

{ error: "error" } or { error: "internal" }

Other failure

null

Timeout (6 s), network error, or sku was an empty string

Events: fires purchase only for a successful answer (ok: true, including alreadyOwned), with that answer as the payload. Errors, null and the timeout never fire it. A successful answer that arrives after the 6-second timeout has already resolved your promise with null still fires the event, so listening for purchase catches late successes.

Idempotency:

  • Repeat purchases of an owned SKU are free no-ops that return alreadyOwned.

  • A purchase is atomic: the item is granted, the player charged and the creator credited together, or not at all. Two purchases of the same SKU in flight at the same time can't both charge. At most one is charged, and the other resolves alreadyOwned (or fails on its own, for example with insufficient_coins).

  • There's no idempotency key.

Sign-in: required. Guests get { error: "sign_in_required" }.

Limits: 60 purchases per minute per player.

Example

buyBtn.onclick = async function () {
  buyBtn.disabled = true;
  const r = await GameSDK.economy.purchase("skin_gold");
  buyBtn.disabled = false;
  if (r && r.ok) { equip("skin_gold"); setBalance(r.coins); return; }
  if (r === null) {                                   // unknown outcome: reconcile
    const e = await GameSDK.economy.getEntitlements();
    if (e && e.owned.indexOf("skin_gold") !== -1) equip("skin_gold");
    return;
  }
  toast(r.error === "insufficient_coins" ? "Not enough coins" :
        r.error === "sign_in_required" ? "Sign in to buy" : "Purchase failed");
};

Pitfalls

  • A null result doesn't mean "not purchased". Reconcile with getEntitlements().

  • Items are durable only. You can't buy a SKU twice, so there are no consumables.

  • There's no refund operation.

  • Coins can't be bought. Don't show a "buy coins" prompt.


GameSDK.economy.getEntitlements()

GameSDK.economy.getEntitlements(): Promise<{ owned: string[] } | null>

The SKUs the signed-in player owns in this game.

Parameters: none.

Resolves: { owned: ["skin_gold", …] }. This includes SKUs whose items have since been deactivated or deleted.

Failure behaviour: resolves { owned: [] } for guests and when the API returns an error, and null on a network error or timeout.

{ owned: [] } doesn't prove the player owns nothing. It's also what you get for a guest or an API error. Don't remove unlocks based on an empty result. Only add unlocks based on owned.

Sign-in: needed for a meaningful result. Guests always get { owned: [] }.

Example

GameSDK.economy.getEntitlements().then(function (e) {
  ((e && e.owned) || []).forEach(unlock);
});

Tournaments

Tournaments are created server-side by the creator. See Tournaments. Scores reach them automatically through GameSDK.submitScore() and GameSDK.replay.submit(). There's no tournament-specific score call.

The Tournament objects below share this shape:

interface Tournament {
  id: string;
  name: string;
  description: string | null;
  metric: "high_score" | "total_score";
  status: "scheduled" | "live" | "ended" | "settled" | "canceled";
  prizePoolCoins: number;          // coins (virtual currency)
  maxWinners: number;
  startsAt: string;                // ISO 8601
  endsAt: string;                  // ISO 8601
}

GameSDK.tournaments.list()

GameSDK.tournaments.list(): Promise<ListedTournament[] | null>

interface ListedTournament extends Tournament {
  entries: number;          // number of players entered
  joined: boolean;          // this player has an entry (joined or scored)
  myRank: number | null;    // this player's current rank; null until they've scored
}

This game's tournaments:

  • scheduled, live and ended ones

  • settled ones for 7 days after settlement

  • up to 20, latest endsAt first

  • never canceled ones

Status changes (scheduled → live, and settlement of tournaments past endsAt) happen in the background about every 30 seconds. Reading the list also applies any that are due.

Parameters: none.

Resolves: an array (possibly empty).

  • myRank uses the same ranking as the standings and settlement: higher score first, then whoever reached that score first. Players in an exact tie (same score reached at the same moment) share a rank, and the next rank is skipped (1, 2, 2, 4). See Tournaments.

  • joined is true for any entry. A player who joined but hasn't scored has joined: true and myRank: null.

Failure behaviour: resolves [] if the API returns an error, and null on a network error or timeout.

Sign-in: not required. For guests, joined is false and myRank is null.

Example

const ts = (await GameSDK.tournaments.list()) || [];
ts.filter(function (t) { return t.status === "scheduled"; })
  .forEach(function (t) { addUpcoming(t.name, new Date(t.startsAt)); });

myRank always matches the player's rank in getStandings(). Use it when the player is outside the top 100 that getStandings() returns.


GameSDK.tournaments.getActive()

GameSDK.tournaments.getActive(): Promise<ListedTournament | null>

A convenience wrapper over list(). It returns the first tournament with status === "live", which is the live one with the latest endsAt, or null.

Parameters: none.

Resolves: one ListedTournament, or null if none is live.

Failure behaviour: null on any failure or timeout. That's indistinguishable from "no live tournament".

Sign-in: not required. Guest behaviour is the same as for list().

Example

GameSDK.tournaments.getActive().then(function (t) {
  if (t) showBanner(t.name + " ends " + new Date(t.endsAt).toLocaleString());
});

GameSDK.tournaments.join(id)

GameSDK.tournaments.join(id: string): Promise<{ ok: true; joined: true } | null>

Enters the signed-in player into a tournament with a score of 0 and no rank, so they appear on the board before scoring. Joining is optional. The first accepted score enters them anyway.

Parameters

Name

Type

Required

Constraints

id

string

Yes

A tournament id. It must be scheduled or live, with endsAt in the future.

Resolves: { ok: true, joined: true }. That's also the result when the player had already joined.

Failure behaviour: resolves null for any of these:

  • the player is a guest

  • the tournament isn't open (closed)

  • the tournament doesn't exist

  • any other error, or a timeout

The specific reason isn't passed to the game.

Events: fires tournamentJoined with { ok: true, joined: true } only when the join succeeded. Failures and timeouts never fire it. A success that arrives after the 6-second timeout still fires the event.

Sign-in: required. Guests get null.

Example

joinBtn.onclick = async function () {
  const r = await GameSDK.tournaments.join(currentTournament.id);
  if (r && r.joined) joinBtn.textContent = "You're in!";
  else toast("Sign in to join, or this tournament has closed.");
};

Pitfall: joining alone never wins anything. A player who joined but never scored has no rank and gets no prize, however few players there are.


GameSDK.tournaments.getStandings(id)

GameSDK.tournaments.getStandings(id: string): Promise<{
  tournament: Tournament & { game: { slug: string; title: string } };
  standings: Standing[];
} | null>

interface Standing {
  rank: number | null;    // null for an entrant who hasn't scored
  handle: string;
  displayName: string | null;
  avatarUrl: string | null;
  score: number;
  prizeCoins: number;     // 0 until settled
}

The full detail and the top 100 standings of any tournament, by id. Reading it settles the tournament straight away if its window has closed and the background job hasn't settled it yet.

Parameters

Name

Type

Required

Constraints

id

string

Yes

Any tournament id. It doesn't have to belong to this game.

Resolves:

  • Standings are ordered by score, highest first, then by who reached that score first. Entrants who haven't scored come last, with rank: null.

  • rank is a competition rank: players in an exact tie share it and the next rank is skipped (1, 2, 2, 4). It's the same rank myRank in list() reports and settlement uses.

  • Before settlement, prizeCoins is 0. After settlement, rank and prizeCoins are the final stamped values. Tied winners show their equal split of the prize.

Failure behaviour: null if the tournament doesn't exist, on an API error, or on a timeout.

Sign-in: not required. The result is the same for everyone.

Example

const r = await GameSDK.tournaments.getStandings(t.id);
if (r) r.standings.slice(0, 10).forEach(function (s) {
  addRow(s.rank ? "#" + s.rank : "–", s.displayName || s.handle, s.score,
         s.prizeCoins ? s.prizeCoins + " coins" : "");
});

Pitfalls

  • Only the top 100 are returned. A player ranked lower won't find themselves here, so use myRank from list().

  • Two rows can share a rank. Don't use rank as a unique key or list index.


Events

Subscribe with GameSDK.on(name, callback). Callbacks that throw are ignored and don't affect other listeners.

Event

Payload

Fired by

adStarted

none

An ad overlay appeared

adComplete

{ rewarded: boolean }

An ad ended or was closed

adError

{ reason: "no_fill" | "no_consent" | "no_session" | "busy" | "timeout" | "error" }

No ad was shown for that request

purchase

The successful purchase() result ({ ok: true, … })

A successful economy.purchase, including alreadyOwned

tournamentJoined

{ ok: true, joined: true }

A successful tournaments.join

pause / resume

none

The browser tab was hidden or shown. Not fired for ads.

Related

Was this page helpful?
GameSDK reference: ads, economy & tournaments