In your game

SDK

The Yes SDK runs inside your game. Submit scores, draw leaderboards, reward players with ads or Tickets, show interstitials and trigger haptics — all from a single global yes object.

Getting Started

No installation needed

The Yes app loads the SDK into your game before your code runs. Just use the global yes object.

Quick Start

// Submit a score (no need to wait for it)
yes.submitScore(score);
// Revive the player for a rewarded ad
const ad = await yes.watchRewardedAd('revive');
if (ad.rewarded) revivePlayer();
// Trigger haptic feedback
yes.haptic('success');
yes.onReady(callback)#
yes.onReady(callback: () => void): void

Runs your callback once the SDK has started. If it already has, the callback runs straight away. You don't need it for getLanguage(), which is correct from your first line of code.

Example
yes.onReady(async () => {
const player = await yes.getPlayer();
showWelcome(player.name);
});
yes.version#
yes.version: string

The SDK version, e.g. '1.10.0'. Handy in bug reports.

Example
console.log(yes.version);

Calls Never Fail

Every async SDK call always resolves — it never rejects. When something goes wrong at runtime (the player is offline, an ad didn't load, the app didn't answer), you get a “didn't happen” value instead: rewarded: false, spent: false, validated: false, null. So you don't need try/catch; just check the result.

The one exception is a bug in your own code. Bad arguments — a score that isn't a number, an empty placement name, a Ticket amount outside 1–10 — throw straight away, so the mistake shows up where you made it.

const { spent } = await yes.spendTicket(3, 'booster');
if (spent) grantBooster(); // otherwise just carry on
yes.submitScore(NaN); // throws at once: fix your code

API Reference

Player

yes.getPlayer()#
yes.getPlayer(): Promise<Player>

Get the current player as other players see them on leaderboards: a name and an avatar. There are no player ids. If the app is still signing the player in, the call waits for it. Both fields are null when nobody is signed in.

Returns

Promise<Player>{ name: string | null, avatarUrl: string | null }

Example
const player = await yes.getPlayer();
nameLabel.textContent = player.name ?? 'Player';

Scores & Leaderboards

Boost player retention

Leaderboards drive competition and keep players coming back. Games with score systems see significantly higher retention rates as players compete to climb the ranks.
yes.submitScore()#
yes.submitScore(score: number, options?: SubmitScoreOptions): Promise<SubmitScoreResult>

Submit a score to the leaderboard. It never blocks your game, so you can call it and move on. If you want the result, await it: it resolves once the server has checked the score. Offline, the score is saved on the device and syncs later.

Parameters
NameTypeDescription
scorenumberThe score, a number of 0 or more. Anything else throws.
options.metadata?objectExtra data to store with the score, e.g. { level: 5 }
Returns

Promise<SubmitScoreResult>isNewBest, rank and score; validated (true when the server confirmed it); snapshot, the fresh leaderboard (or null). Offline it resolves { validated: false, queued: true }.

Example
// Fire and forget
yes.submitScore(gameState.score);
// Or wait for the server's answer
const result = await yes.submitScore(gameState.score);
if (result.validated && result.isNewBest) {
showBanner(`New best! Rank #${result.rank}`);
}
yes.getLeaderboard()#
yes.getLeaderboard(): Promise<LeaderboardSnapshot | null>

Get the game's leaderboards so you can draw your own board. You get two boards: weekly (resets every week) and allTime. Each has the top 10 (top), the player's own row (self, or null if they haven't scored) and the players just above and below them (neighbours). Calling it again is cheap, and calling it right after submitScore() gives you the board with the new score. Resolves null when there's no leaderboard to show, e.g. the player is signed out — skip your leaderboard UI then.

Returns

Promise<LeaderboardSnapshot | null>Rows are { name, avatarUrl, score, rank, isSelf, entryId }. weekly also has endsAt (when it resets) and now (server time), for a countdown. stale: true means it may be missing the latest score (e.g. offline).

Example
const board = await yes.getLeaderboard();
if (board) {
const me = board.weekly.self;
showRank(me ? `#${me.rank} this week` : 'Play to get on the board');
for (const row of board.weekly.top) {
drawRow(row.rank, row.name, row.score, row.isSelf);
}
}

Reward Placements

Your game owns the offer

A Reward Placement is a named spot in your game where the player can watch a rewarded ad or spend Tickets for a reward you define: a revive, a booster, extra moves. You draw the offer in your own UI, set the Ticket price and grant the reward. The platform draws no screen of its own, and there are no platform caps on how often you offer.

Every call takes a placement name (e.g. 'revive'): a non-empty string of at most 64 characters. Use the same name for one spot every time.
yes.isRewardedAdReady()#
yes.isRewardedAdReady(): Promise<RewardState>

Read what your offer needs to draw itself: whether a rewarded ad is ready, and how many Tickets the player holds.

Returns

Promise<RewardState>{ ready: boolean, ticketBalance: number }. { ready: false, ticketBalance: 0 } if the app can't answer.

Example
const { ready, ticketBalance } = await yes.isRewardedAdReady();
watchAdButton.disabled = !ready;
ticketLabel.textContent = `You have ${ticketBalance} Tickets`;
yes.onRewardStateChange(callback)#
yes.onRewardStateChange(callback: (state: RewardState) => void): () => void

Fires when ad readiness or the Ticket balance changes, so you redraw your offer instead of polling. Returns an unsubscribe function.

Returns

() => voidCall it to stop listening, e.g. when the offer closes.

Example
drawOffer(await yes.isRewardedAdReady());
const off = yes.onRewardStateChange(state => drawOffer(state));
// ...when the offer closes
off();
yes.reportRewardOffered(placement)#
yes.reportRewardOffered(placement: string): void

Call it every time an offer appears on screen, whether or not the player takes it. No need to await. Without it the platform can't tell a placement nobody takes from one nobody sees.

Parameters
NameTypeDescription
placementstringThe Reward Placement name
Example
showReviveOffer();
yes.reportRewardOffered('revive');
yes.watchRewardedAd(placement)#
yes.watchRewardedAd(placement: string): Promise<RewardedAdResult>

Play a rewarded ad straight away, with no platform screen. Your game is paused while it plays. Grant the reward only when rewarded is true. Watching an ad never gives the player Tickets.

Parameters
NameTypeDescription
placementstringThe Reward Placement name
Returns

Promise<RewardedAdResult>{ rewarded: boolean }, plus errorCode ('NOT_LOADED' | 'USER_DISMISSED' | 'FAILED_TO_SHOW' | 'UNKNOWN') when not rewarded

Example
const result = await yes.watchRewardedAd('revive');
if (result.rewarded) {
revivePlayer();
}
yes.spendTicket(amount, placement)#
yes.spendTicket(amount: number, placement: string): Promise<TicketSpendResult>

Spend Tickets at the price you set. Each call is one spend; calling again charges again. If the player doesn't hold enough, the Yes Store opens with your game paused behind it, and the call resolves once it closes: spent if they bought enough, otherwise INSUFFICIENT_BALANCE.

Parameters
NameTypeDescription
amountnumberTickets to spend, a whole number from 1 to 10
placementstringThe Reward Placement name
Returns

Promise<TicketSpendResult>{ spent: boolean }, plus errorCode ('INSUFFICIENT_BALANCE' | 'FAILED') when not spent

Example
const result = await yes.spendTicket(3, 'booster');
if (result.spent) {
grantBooster();
}

Don't hide a Ticket button on a short balance

Tapping it when the player can't afford it is how they reach the Store. Show the price, and let spendTicket() handle the rest.

Example: revive (ad or Tickets)

async function onGameOver() {
const offer = showReviveOffer({ price: 1 }); // your own UI
yes.reportRewardOffered('revive');
const draw = ({ ready }) => offer.setAdEnabled(ready);
draw(await yes.isRewardedAdReady());
const off = yes.onRewardStateChange(draw);
offer.onWatchAd(async () => {
const { rewarded } = await yes.watchRewardedAd('revive');
if (rewarded) { off(); offer.close(); revivePlayer(); }
});
offer.onPayTickets(async () => {
const { spent } = await yes.spendTicket(1, 'revive');
if (spent) { off(); offer.close(); revivePlayer(); }
});
offer.onDecline(() => { off(); offer.close(); showResults(); });
}

Example: booster (Tickets only)

A placement doesn't have to offer an ad. Price it at what the reward is worth.

async function openBoosterOffer() {
const offer = showBoosterOffer({ price: 3 }); // your own UI
yes.reportRewardOffered('booster');
offer.setBalance((await yes.isRewardedAdReady()).ticketBalance);
const off = yes.onRewardStateChange(({ ticketBalance }) => offer.setBalance(ticketBalance));
offer.onClose(off);
offer.onBuy(async () => {
const { spent } = await yes.spendTicket(3, 'booster');
if (spent) grantBooster();
});
}

Interstitial Ads

You name the break, the platform decides

Call showInterstitialAd() at a natural break — a level end, a game over, a return to the menu. The platform decides whether an ad actually plays, and paces ads across every game the player plays, so don't add a cooldown or counter of your own. For opt-in rewarded ads, see Reward Placements.
yes.showInterstitialAd(placement?)#
yes.showInterstitialAd(placement?: string): Promise<InterstitialAdResult>

Tell the platform this is a good moment for an ad. If no ad fits or none is loaded, it resolves 'skipped' straight away. If an ad plays, your game is paused behind it and the call resolves when the ad closes. Either way, carry on the same. Only call it at a real break, never mid-action.

Parameters
NameTypeDescription
placement?stringA label for your analytics, e.g. 'level_end'. At most 64 characters.
Returns

Promise<InterstitialAdResult>{ status: 'shown' | 'skipped' }, plus a reason when skipped, for logging only. Don't change your game's behaviour based on it.

Example
async function onLevelComplete() {
showResults();
await yes.showInterstitialAd('level_end');
showNextLevelButton(); // the same whether or not an ad played
}
yes.isInterstitialAdReady()#
yes.isInterstitialAdReady(): Promise<AdReadyStatus>

Whether an interstitial is loaded right now. Just a hint: you don't need to call it before showInterstitialAd(), which may still skip.

Returns

Promise<AdReadyStatus>{ ready: boolean }

Example
const { ready } = await yes.isInterstitialAdReady();

Games that never call showInterstitialAd() still earn: the platform may show an interstitial after a score submit. Once your game calls it, the platform leaves ad timing to your placements.

Haptic

yes.haptic()#
yes.haptic(style?: HapticStyle): void

Trigger haptic feedback on the player's device. No need to await. Does nothing on devices without haptics.

Parameters
NameTypeDescription
style?'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error' | 'selection'The haptic feedback style. Defaults to 'medium'
Example
yes.haptic(); // default medium tap
yes.haptic('success'); // positive feedback
yes.haptic('error'); // negative feedback

Language

Match the app's language

Players set their language in the Yes app. Use getLanguage() to read it and localize your game's UI so everything feels native to them.
yes.getLanguage()#
yes.getLanguage(): string

Returns the app's current language code. Synchronous, and already correct on your first line of game code — no need to wait for onReady().

Returns

stringOne of 'en', 'tr', 'es', 'pt-BR', 'de', 'fr', 'ar'. Defaults to 'en'. Note that 'pt-BR' is region-tagged and 'ar' is right-to-left.

Example
const lang = yes.getLanguage();
const base = lang.split('-')[0]; // 'pt-BR' -> 'pt'
setUILanguage(SUPPORTED[base] ? base : 'en');
yes.onLanguageChange(callback)#
yes.onLanguageChange(callback: (language: string) => void): () => void

Fires when the player switches language while your game is running. Only on an actual change — read the initial value with getLanguage(). Returns an unsubscribe function.

Returns

() => voidCall it to stop listening.

Example
// Localize now, and again whenever the player switches.
applyTranslations();
yes.onLanguageChange(() => applyTranslations());

Never freeze the language at module scope

This is the most common localization bug we see. Capturing getLanguage() once into a module-level constant means your HUD and menus keep whatever language they had at startup, while text you render later comes out correct — so the game looks half-translated. Read it where you use it, or re-render from onLanguageChange().

Saving Progress

Save your game's progress (levels, items, unlocks) with plain localStorage. On Yes it is backed up to the player's account and follows them to other devices, and it works offline. There is no extra API to call.

Each game gets up to 256KB per player. If a write goes over, your game keeps its local copy but the server refuses it, and you get a yes:storagerejected event on window.

localStorage.setItem('progress', JSON.stringify({ level: 12 }));
window.addEventListener('yes:storagerejected', (e) => {
// e.detail: { reason: 'cap_exceeded', capBytes, currentBytes }
console.warn('Save is over the 256KB limit', e.detail);
});

Testing Locally

Outside the Yes app, every SDK call resolves its “didn't happen” value at once: no player, no leaderboard, no ads, no Tickets. Your game should still run.

To see your game with real-looking data, load the mock host before the SDK. It stands in for the Yes app and ships in the @aspect_games/yes-sdk npm package as dist/yes-mock-host.js. Never upload it with your game.

<script src="yes-mock-host.js"></script>
<script src="yes-sdk.js"></script>

The mock host gives you a player named TestPlayer, a sample leaderboard, ads that always play, and 10 Tickets to spend (spends past that fail with INSUFFICIENT_BALANCE). To test pausing, run yesMockHost.command({ type: 'pause', gameId: 0 }) in the browser console.

Full Example

A complete game.js that uses every SDK feature:

game.js
let lives = 3;
let score = 0;
// Localize straight away, and again if the player switches
applyTranslations(yes.getLanguage());
yes.onLanguageChange(lang => applyTranslations(lang));
// Greet the player
yes.onReady(async () => {
const player = await yes.getPlayer();
document.getElementById('playerName').textContent = player.name ?? 'Player';
});
// Called when the player completes a level
async function onLevelComplete(levelScore) {
score += levelScore;
yes.submitScore(score); // no need to wait
yes.haptic('success');
localStorage.setItem('score', String(score)); // saved to the player's account
showResults();
await yes.showInterstitialAd('level_end'); // may or may not play an ad
loadNextLevel();
}
// Show the leaderboard on the results screen
async function showBoard() {
const board = await yes.getLeaderboard();
if (board) drawLeaderboard(board.weekly);
}
// Reward Placement: an extra life for an ad or 1 Ticket
function showExtraLifeOffer() {
drawExtraLifeOffer(); // your own UI
yes.reportRewardOffered('extra_life');
}
async function watchAdForLife() {
const result = await yes.watchRewardedAd('extra_life');
if (result.rewarded) lives++;
}
async function payTicketForLife() {
const result = await yes.spendTicket(1, 'extra_life');
if (result.spent) lives++;
}

Best Practices

Check results, not errors

SDK calls never reject, so skip the try/catch. Grant rewards only when the result says so:

const ad = await yes.watchRewardedAd('revive');
if (ad.rewarded) {
giveReward();
}

Score submission timing

Submit scores at natural game moments — end of level, game over, or achievement completion. Avoid spamming submissions during gameplay.

Ads only at real breaks

Call showInterstitialAd() between levels or on game over, never mid-action. Continue the same way whether or not an ad played.

Test with the mock host

Develop with the mock host loaded, then remove it before you upload.