Ads and rewarded ads

Embed
Preview of the card
<a href="https://coolgptgames.com/guides/ads-and-rewards"><img src="https://coolgptgames.com/guides/_api/public/orgs/062b11d9-2b86-4627-9e86-96816a67d7a1/spaces/680f0c4e-f8bc-4af0-983c-c373db913597/pages/5299e807-3028-45fc-a6ff-437c81dfd331/social-card.svg?v=3e7ae33eec8b" alt="Ads and rewarded ads" width="600" height="315"></a>

Your game never loads ad code itself. It asks for an ad with GameSDK.requestAd(). The Cool GPT Games page that hosts your game then fetches the ad, shows it in an overlay on top of your iframe, and tells your game how it went. Because of this, a game can't fake an impression, pick an ad network, or grant itself a reward that the ad server didn't confirm.

This guide covers:

  • the two kinds of ad you can request

  • the lifecycle events and how to pause around an ad

  • what a "rewarded" result guarantees, and how your own server verifies it

  • frequency caps and limits

  • how the visitor's consent and age answers change what's served

  • how ad revenue relates to creator earnings

For exact signatures, resolved values and failure modes, see GameSDK reference: ads, economy & tournaments.

Quick start

// A "watch an ad for an extra life" button.
continueButton.onclick = async function () {
  continueButton.disabled = true;
  const result = await GameSDK.requestAd("rewarded");
  continueButton.disabled = false;

  if (result && result.rewarded === true) {
    grantExtraLife();          // the ad server confirmed the reward
  } else {
    showMessage("No reward this time.");
  }
};

// Pause and mute while any ad is on screen.
GameSDK.on("adStarted", function () { pauseGame(); muteAudio(); });
GameSDK.on("adComplete", function () { resumeGame(); unmuteAudio(); });
GameSDK.on("adError", function (e) {
  if (e.reason !== "busy") { resumeGame(); unmuteAudio(); }  // "busy": another ad is still showing
});

Placements

requestAd(placement) accepts one argument. The SDK recognises exactly one value, "rewarded". Any other value, including no value, is sent as a "midroll" request.

You pass

What is requested

What the player sees

Needs a play session

Can grant a reward

"rewarded"

Rewarded ad

An "Advertisement" card over the game with a 5-second "Reward in Ns" countdown and a close (✕) button. The creative may be clickable and opens in a new tab.

No. It works from a menu before gameplay starts.

Yes, when the ad server confirms it

anything else ("midroll", "interstitial", undefined, …)

Midroll (interstitial) ad

An "Ad" overlay reading "Your game continues shortly…" for about 2 seconds

Yes. See Midroll needs a session.

Never. The result is always { rewarded: false }.

The platform also defines preroll and banner placements, but you can't request them from the game SDK. Site banners are placed around the player by the page, not by your game.

Midroll is implemented but not switched on yet. The whole path works, but no interstitial ad tag is configured, so a midroll request resolves { error: "not_configured" } today and adError fires with { reason: "not_configured" }. Rewarded ads work now. Treat not_configured and no_fill as normal outcomes, and never block progress on a midroll ad.

Midroll needs a session

A midroll request is sent through the Cool GPT Games API and tied to the current play session. The page starts that session when your game calls GameSDK.start() (or GameSDK.replay.ready()). If you request a midroll before either call, it fails right away: the promise resolves { error: "no_session" } and adError fires with { reason: "no_session" }.

Rewarded ads have no such requirement.

How an ad is served

Every in-game ad goes through the same three steps, and your game sees only the last one:

  1. Authorisation. The page asks the Cool GPT Games API for permission to show one ad. That's where the frequency caps are enforced. A request over the cap is refused here, before any ad is fetched. 2. Fill. The page fetches the creative in the visitor's browser and shows it in an overlay on top of your iframe. 3. Outcome. The page reports back what happened — shown, completed, skipped, rewarded, no fill or error. That report is what books the creator's ad revenue and, for a rewarded ad, mints the reward receipt described below.

Lifecycle

Every requestAd call settles exactly once, with exactly one outcome: either adComplete or adError fires, never both. Along the way it may also fire adStarted. You subscribe to these events with GameSDK.on(name, callback):

Event

Payload

Fired when

adStarted

none

The ad overlay is now covering your game

adComplete

{ rewarded: boolean, rewardId?: string }

The ad finished or the player closed it

adError

{ reason: string, retryAfterSec?: number }

No ad could be shown, or the request timed out

Sequences you will see

Situation

Events, in order

Promise resolves to

Rewarded, watched to the end, reward confirmed

adStarted → adComplete {rewarded:true, rewardId}

{ rewarded: true, rewardId }

Rewarded, watched to the end, ad server declined the reward

adStarted → adComplete {rewarded:false}

{ rewarded: false }

Rewarded, player closed it early (✕)

adStarted → adComplete {rewarded:false}

{ rewarded: false }

Midroll shown

adStarted → (~2 s) → adComplete {rewarded:false}

{ rewarded: false }

Too soon after the last ad of that placement

adError {reason:"capped", retryAfterSec}

{ error: "capped", retryAfterSec }

This player has used up their allowance of that placement

adError {reason:"capped"}

{ error: "capped" } (no retryAfterSec)

Midroll requested while no interstitial tag is configured

adError {reason:"not_configured"}

{ error: "not_configured" }

No ad available

adError {reason:"no_fill"}

{ error: "no_fill" }

Midroll before GameSDK.start()

adError {reason:"no_session"}

{ error: "no_session" }

Visitor hasn't allowed ads

adError {reason:"no_consent"}

{ error: "no_consent" }

Another ad is already running

adError {reason:"busy"} (the running ad carries on)

{ error: "busy" }

No answer within 60 seconds

adError {reason:"timeout"} (fired by the SDK)

{ error: "timeout" }

Network or other failure

adError {reason:"error"}

{ error: "error" }

adStarted doesn't fire in any of the error cases, so you only need to pause from adStarted, not from the moment you call requestAd.

A busy answer is about the new request only. The ad that was already running still ends with its own adComplete or adError, so don't resume your game on a busy error.

Pausing and resuming around ads

The platform does not pause your game for you when an ad starts. Your game keeps running underneath the overlay. Pause gameplay, timers and audio on adStarted, and resume on adComplete or adError.

Separately, the SDK fires pause and resume when the browser tab is hidden or shown again. Those events come from tab visibility, not from ads. Handle them too, but don't rely on them for ads.

let adPaused = false;
GameSDK.on("adStarted", function () { adPaused = true; game.pause(); audio.mute(); });
function endAd() {
  if (!adPaused) return;          // nothing was paused (the ad never started)
  adPaused = false;
  game.resume(); audio.unmute();
}
GameSDK.on("adComplete", endAd);
GameSDK.on("adError", function (e) { if (e.reason !== "busy") endAd(); });

// Tab visibility, independent of ads:
GameSDK.on("pause", function () { game.pause(); });
GameSDK.on("resume", function () { if (!adPaused) game.resume(); });

What "rewarded" guarantees

{ rewarded: true, rewardId } means all of the following happened:

  1. A rewarded ad was authorised, fetched and shown to the player.

  2. The player left it open until the 5-second countdown reached zero.

  3. The page then redeemed the ad server's single-use reward token, and the ad server replied that the reward was granted.

  4. The platform recorded that confirmation as a reward receipt and handed your game its rewardId.

rewarded is never true when the player closed the ad early, when no ad filled, or when the redemption call failed or was declined. Each ad's reward token can be redeemed only once.

What it does not guarantee:

  • It doesn't grant anything on its own. The platform sends no coins, XP or items for a rewarded ad. Your game decides what the reward is and grants it when it sees rewarded === true.

  • It doesn't mean the player is signed in. Rewarded ads work for guests too, and the receipt then carries playerId: null.

Verifying a reward on your own server

rewardId is a single-use receipt. If your game has a backend and the reward is worth protecting (a server-held currency, a persistent unlock), don't trust the client: send the rewardId to your server and check it against the platform.

const r = await GameSDK.requestAd("rewarded");
if (r.rewarded === true && r.rewardId) {
  await fetch("/my-backend/claim-reward", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ rewardId: r.rewardId }),
  });
}

Your server then calls, once per receipt:

GET https://api.coolgptgames.com/v1/dev/games/:id/ad-rewards/:rewardId

:id is your game's id, not its slug. Authenticate with an API key that has the economy or analytics scope (a creator session for a game you own also works). See API keys & scopes.

curl -s "https://api.coolgptgames.com/v1/dev/games/$GAME_ID/ad-rewards/$REWARD_ID" \
  -H "authorization: Bearer $API_KEY"
{
  "reward": {
    "rewardId": "rwd_9Qk2xR7tLpA3nV0sHb4Zu1",
    "gameId": "0192f3a1-7c4e-7b21-9a5d-3f7e1c2b4d68",
    "sessionId": "0192f3b0-2d18-7c44-8e11-9ab3c4d5e6f7",
    "playerId": "0192a77c-11de-7f03-b4c2-5d8e9f0a1b23",
    "placement": "rewarded",
    "grantedAt": "2026-09-21T10:15:04.812Z",
    "firstVerifiedAt": "2026-09-21T10:15:05.190Z"
  },
  "alreadyVerified": false
}

Field

Meaning

reward.rewardId

The receipt you asked about

reward.gameId

Your game. A receipt from another game returns 404 not_found.

reward.sessionId

The play session the ad ran in, or null for an ad watched from a menu with no session

reward.playerId

The signed-in player, or null for a guest

reward.grantedAt

When the ad server confirmed the reward (ISO 8601)

reward.firstVerifiedAt

When this receipt was first verified

alreadyVerified

false on the first call, true on every call after it

alreadyVerified is the replay check. The first call stamps the receipt; every later call for the same rewardId returns alreadyVerified: true. Grant the reward only when it is false, and reject the claim otherwise. A 404 means the receipt doesn't exist or belongs to another game — reject that too.

// Node (18+), inside your own backend.
export async function claimReward(rewardId, playerId) {
  const res = await fetch(
    `https://api.coolgptgames.com/v1/dev/games/${process.env.GAME_ID}/ad-rewards/${encodeURIComponent(rewardId)}`,
    { headers: { authorization: `Bearer ${process.env.COOLGPTGAMES_API_KEY}` } },
  );
  if (!res.ok) return { ok: false, reason: "unknown_reward" };   // 404: forged or wrong game

  const { reward, alreadyVerified } = await res.json();
  if (alreadyVerified) return { ok: false, reason: "already_claimed" };
  if (reward.playerId && reward.playerId !== playerId) return { ok: false, reason: "wrong_player" };

  await grantExtraLife(playerId);                                // your own game logic
  return { ok: true, grantedAt: reward.grantedAt };
}

Checking reward.playerId only works for signed-in players. For guests it's null, so treat a guest reward as good for that browser session only.

The ad.rewarded webhook. If you'd rather be pushed than polled, subscribe a webhook to the ad.rewarded event. Its payload is the same receipt, minus the verification stamp:

{
  "rewardId": "rwd_9Qk2xR7tLpA3nV0sHb4Zu1",
  "gameId": "0192f3a1-7c4e-7b21-9a5d-3f7e1c2b4d68",
  "sessionId": "0192f3b0-2d18-7c44-8e11-9ab3c4d5e6f7",
  "playerId": "0192a77c-11de-7f03-b4c2-5d8e9f0a1b23",
  "placement": "rewarded",
  "grantedAt": "2026-09-21T10:15:04.812Z"
}

The webhook tells you a reward happened; the verify call is still what makes a claim single-use. See Webhooks.

Frequency and limits

Frequency caps are enforced by the platform, not just recommended. They're counted against the current play session; for a rewarded ad watched with no session (from a menu), they're counted per viewer per game over a rolling 30-minute window instead.

Placement

Cap

What your game sees over the cap

Rewarded

12 per session, and at least 15 seconds apart

{ error: "capped" }

Midroll (interstitial)

4 per session, and at least 3 minutes apart

{ error: "capped" }

Preroll

1 per session (not requestable from the SDK)

n/a

A request that never gets an outcome reported still counts, so a game can't dodge the cap by dropping the ad. A request that ends in no_fill or an error doesn't count — the player never saw an ad.

retryAfterSec tells you when to offer the ad again. When the spacing rule is what blocked the request, the answer carries the whole seconds left until the next ad of that placement is allowed:

const r = await GameSDK.requestAd("rewarded");
if (r.error === "capped") {
  if (r.retryAfterSec) {
    adButton.disabled = true;
    setTimeout(function () { adButton.disabled = false; }, r.retryAfterSec * 1000);
  } else {
    adButton.remove();            // allowance used up for this session
  }
}

It's absent when the player has simply used up their per-session allowance, because there's nothing to wait for: the count doesn't reset until the next play session. So treat "no retryAfterSec" as "not again this session", and never invent a retry delay of your own.

Limit

Value

Effect on your game

One ad at a time

The page runs one ad at a time

Calling requestAd while an ad is running resolves { error: "busy" } straight away. The running ad isn't affected. Disable your ad button until the call settles.

Timeout

60 seconds

If the page doesn't finish the ad within 60 seconds (for example, the message was dropped), the promise resolves { error: "timeout" } and adError {reason:"timeout"} fires.

Messages to the page

30 SDK messages per second, across all SDK calls

Extra messages are silently dropped. A dropped requestAd resolves { error: "timeout" } after 60 seconds.

Ad requests and ad events

60 per minute per play session (or per viewer per game, when there's no session)

Each request logs several events, so a tight loop will be throttled and start failing with error.

Otherwise, treat capped like any other "no ad" answer: move on quietly, and don't retry in a loop. Show midrolls only at natural breaks such as between levels or after a game over. Never show one during active play.

The site asks visitors for ad consent with a cookie banner. Personalised ads also require an age question ("Are you 13 or older?"). Your game can't read or change any of this. It only sees how it affects requestAd:

Visitor's state

What the game sees

Hasn't answered the consent banner yet, the banner couldn't load (for example, it was blocked), or ads were rejected

No ad is requested. adError {reason:"no_consent"} fires and the promise resolves { error: "no_consent" }.

Allowed ads but not personalised ads, or is under 13, or hasn't declared an age

Contextual (non-personalised) ads only. The game sees normal behaviour.

Allowed personalised ads and declared 13 or older

Personalised ads may be served. The game sees normal behaviour.

Game played through an embed on another website

Contextual ads only, because the host site owns consent there. The game sees normal behaviour.

In practice:

  • no_consent is common and expected, especially early in a visit before the banner is answered. The state is checked again on every requestAd call, so a later request can succeed once the visitor answers.

  • Don't nag. Don't loop on requestAd after no_consent, and don't tell the player to change their cookie settings to get a reward. Offer the reward another way, or just hide the button.

  • Personalisation doesn't change the API. Your code is identical whether an ad is personalised or contextual. Personalised serving is enabled on the site today, so which one a player gets depends on their own consent and age answers.

Ads and creator revenue

Creators can earn a share of ad revenue from their games. Everything here is creator-side. Players never earn money from ads or from anything else on the platform.

How revenue is attributed:

  • Only ad fills the platform recorded itself count. Revenue is booked against the ad slot the platform authorised, from the outcome the player page reported — an impression for an interstitial, a confirmed reward for a rewarded ad. Nothing your game reports feeds it, and each slot books at most once, so you can't inflate it from the client. - Today's figures are estimates. Ads filled in the visitor's browser are valued with a configured eCPM (revenue per 1,000 impressions) rather than the network's own reporting, which lags. Those rows are labelled so they can be reconciled against the network's real numbers later, so the amount you see for recent days can change. - Only valid play sessions count. An ad fill is attributed to your game only when it happened inside a play session the platform treats as genuine play. Sessions flagged as abusive, and ad events with no play session (such as a rewarded ad watched from a menu before GameSDK.start()), aren't counted.

  • Revenue is aggregated daily per game, then split between you and the platform. Your dashboard shows the current creator revenue-share percentage, your lifetime, last-30-day and available earnings, and any pending payouts. - Payouts are made periodically to a verified payout account that you connect in your dashboard. They're subject to a minimum payout amount and a hold period for new creator accounts. Amounts below the minimum roll over. The minimum is shown in your dashboard.

Ad earnings are real money paid to creators. They're completely separate from coins, the platform's virtual currency. Coins you earn from item sales or tournament refunds aren't converted to ad earnings, and ad earnings aren't paid in coins. See In-game economy.

No specific earnings are promised. Revenue depends on fill, the visitor's consent state, geography, the ad network and play volume, and may be zero.

Best practices

  • Offer rewarded ads as an opt-in choice ("Watch an ad to continue?"). Never force a rewarded ad.

  • Show a midroll only at a natural break, never mid-action and never right after load.

  • Always handle capped, not_configured, no_fill, no_consent, no_session, timeout and error by moving on. The game must be fully playable with zero ads.

  • On capped, re-enable your ad button from retryAfterSec when it's there, and hide it for the session when it isn't.

  • Disable the ad button while a request is pending, since only one ad can run at a time (a second request gets busy).

  • Grant rewards only on result.rewarded === true, not on adComplete alone and not on adStarted.

  • If the reward is worth more than an in-game convenience, verify its rewardId from your own server before granting it, and refuse a receipt that comes back alreadyVerified: true.

Updated