Yonest SDK guide
Yonest runs your HTML game inside the platform's iframe shell. The top bar (Back to Yonest, Leaderboard, Restart, Fullscreen) is provided by the platform, and no platform code is ever written into your file. You only need to add a few optional calls guarded by if (window.Yonest) and wrap them in marker comments — that way you get the leaderboard and back entry point on Yonest, and can remove everything in one verifiable pass when you move to another platform.
This guide applies to works that run on-site: single-file HTML (hosted) and web bundles (web). Download bundles (download) don't run on-site, so they don't load the SDK — there is no shell, leaderboard or game token.
How your game runs on the platform
When a player clicks “Start game” they land on a platform page, not on your file. The platform puts a persistent top bar at the top of that page and loads your game in an iframe below it; your file lives at a different URL and is read directly by the iframe.
| URL | Contents | Platform injection |
|---|---|---|
| /g/<slug> | The game shell page: a top bar plus the iframe that loads the game — what the player actually sees | The shell itself is a platform page |
| /play/<slug> | The HTML file you uploaded | 0 bytes |
At runtime the shell attaches platform capabilities to the iframe's window, so from your file's point of view the platform is just “a window.Yonest that happens to exist”. That is also what makes “portability” possible: the file contains no platform scaffolding, only the optional calls you wrote yourself.
Three ground rules before integrating
You don't need to put the SDK into your own file
The platform loads yonest-sdk.js and installs it on the game window. All you do is call methods on window.Yonest from inside your game — don't copy this file into your HTML or import it yourself.
You don't need to implement sign-in or authorization
The player's identity comes from the platform session cookie; the game can't get player credentials and doesn't need them. The platform already guarantees that signed-out users are not let through, so your code should never contain its own login logic.
Every platform call must be removable in one pass
Put each platform call inside its own if (window.Yonest) check and wrap them in the comment markers below. The markers do nothing at runtime; they simply turn “removing platform features” into one verifiable deletion.
/* yonest:start */
if (window.Yonest) window.Yonest.submitScore(score);
if (window.Yonest) window.Yonest.gameOverActions(overlayEl);
/* yonest:end */
When you open this file outside the platform, window.Yonest doesn't exist, so these calls are blocked by the if and skipped silently, and the game runs as usual.
Minimal integration
The most typical integration point is “end of a run”: hand the score to the platform and add one more back entry point to your own game-over dialog. The code below can be copied straight into your existing game-over handler.
// Call when the round ends (overlayEl is your own results overlay)
function onGameOver(score, overlayEl) {
/* yonest:start */
if (window.Yonest) {
if (window.Yonest.supportsLeaderboard) {
window.Yonest.submitScore(score).then(function (r) {
console.log(r.improved ? 'New record ' + r.best : 'Best ' + r.best);
}).catch(function () { /* silently skip when not signed in / not acquired */ });
}
window.Yonest.gameOverActions(overlayEl);
}
/* yonest:end */
}
There is one more equally required integration point: registering the dictionary table (called once when the game initialises). Skip it and players who switch to English or German still see your source text.
// Call once at init: the dictionary table = every player-facing string in the game
/* yonest:start */
var yonestTable = [
['key', 'zh', 'en', 'de', 'fr', 'es', 'ru', 'ja', 'ko' ],
['start', '开始', 'Play', '', '', '', '', '', '' ],
['score', '分数', 'Score', '', '', '', '', '', '' ],
];
function initYonestI18n(Yonest) {
Yonest = Yonest || window.Yonest;
if (!Yonest) {
// not injected yet (the shell installs it after iframe load): queue it, it replays on install
(window.YonestQueue = window.YonestQueue || []).push(initYonestI18n);
return;
}
Yonest.i18n(yonestTable, 'zh'); // an empty cell = not translated, falls back to the source language (zh)
Yonest.applyI18n(); // translates data-i18n markers in the DOM
document.documentElement.lang = Yonest.textLocale; // the language actually rendered, NOT locale
}
/* yonest:end */
initYonestI18n();
Why the queue? The shell injects window.Yonest only after the game’s iframe has finished loading, so during your init code it usually does not exist yet — if (window.Yonest) then silently skips the registration, leaving the dictionary empty (the Maker Console marks every platform language as missing). Push the init call into window.YonestQueue as shown: the SDK replays it the moment it is installed, and anything pushed after install runs immediately.
In the header, list all of en de fr es ru ja ko zh at once (in the same order as Yonest.platformLocales); leave translations you are unsure about empty — an empty cell only falls back, it does not error. Registered languages show up under “Supported languages” on the game page.
| Step | What to do | Result |
|---|---|---|
| 1 | Call submitScore(score) when the game ends | The score enters the leaderboard, and the top bar shows a “this run / new record” notice |
| 2 | Pass your game-over overlay to gameOverActions(overlayEl) | A “Back to Yonest” button is added to the overlay |
| 3 | Wrap the two calls above with yonest:start / yonest:end | You can strip them out in one step later, and the game still runs standalone |
| 4 | Required: register translations with Yonest.i18n(dictionary, sourceLang), then call applyI18n() | Players on other languages see the translation (not the source text) |
Hand integration to an AI
Step 4 above (the dictionary table) is the easiest to miss — because it never errors, it just degrades silently: the game still runs, but players on other languages keep seeing your source text. The block below can be pasted as-is into your AI (Trae / Cursor / Claude, etc.), and it will build the dictionary table for you.
Following the Yonest SDK Guide, integrate this web game into the platform and complete the localisation in one pass:
1. Find every player-facing string in the game (buttons, prompts, results, tutorial, etc.) and put them into one dictionary table whose header lists all platform languages in order: ['key','zh','en','de','fr','es','ru','ja','ko'].
2. Call this once, when the game initialises:
window.Yonest.i18n(dictionary, '<source language>'); // source language = the language your current strings are in
window.Yonest.applyI18n(); // translates data-i18n markers in the DOM
document.documentElement.lang = window.Yonest.textLocale; // NOT locale
Mark page strings with data-i18n="<key>", or look words up with window.Yonest.t('<key>').
3. Leave cells you are unsure about empty (they fall back to the source language, no error), but the header must list all of en de fr es ru ja ko zh.
4. Always wrap platform calls in if (window.Yonest) and in /* yonest:start */ … /* yonest:end */.
5. Final self-check: Yonest.missingLocales() should return an empty array; then tell me how to verify it on the platform.
6. Timing: the shell injects window.Yonest only after the iframe loads. If it is missing at init, push the setup call into window.YonestQueue — (window.YonestQueue = window.YonestQueue || []).push(fn) — so the SDK replays it on install.
Tip: the full list of languages the platform supports is in Yonest.platformLocales; you can also see the platform’s “missing X, Y translations” badge on the game’s card in the Maker Console.
API reference
Every method hangs off window.Yonest. Check that it exists before calling — this is the key to letting the game run in environments without the platform.
Environment and identity
| Property | Type | Description |
|---|---|---|
| game | string | The current game's slug |
| user | string | null | The player's display name; can be used directly in strings like “Hi, XX” |
| supportsLeaderboard | boolean | Whether this game has score reporting enabled; determines whether submitScore exists |
| version | string | SDK version number |
| shellBuild | number | null | Build number of the shell page that installed the SDK; null when a cached older shell page did not send it |
Score reporting
| Item | Description |
|---|---|
| score | number; must be finite. Prefer a single “higher is better” value (score, level count, speed, etc.) |
| options.silent | Pass true to skip the notice in the platform top bar — handy for reporting several times within one run |
| Returns | A Promise resolving to { ok, score, best, improved }: best is this player's all-time best, and improved says whether this submission set a new record |
| Failure | The Promise is rejected, and error.message carries the reason returned by the server (usually in Chinese, e.g. “get this game before you can submit a score”); always add .catch() |
window.Yonest.submitScore(1280).then(function (r) {
// r = { ok: true, score: 1280, best: 1280, improved: true }
});
This member only exists when supportsLeaderboard is true. Check for it together before calling, e.g. if (window.Yonest && window.Yonest.supportsLeaderboard) { ... }; games without a leaderboard don't provide this method, and calling it directly throws a TypeError.
Shell and navigation
| Method | Effect |
|---|---|
| back() | Go back to this game's page on Yonest (leave a review, view the leaderboard, keep browsing the Store) |
| openLeaderboard() | Jump to the leaderboard section of the game page |
| restart() | Reload the game — equivalent to “play another round” |
| setChromeVisible(bool) | Collapse or expand the secondary buttons in the top bar. When collapsed, “Back to Yonest” stays, so players are never trapped |
Back button in your game-over dialog
| Item | Description |
|---|---|
| container | Your game-over overlay element (or any container inside it). The button is appended to its end |
| Returns | The inserted button element; returns null if the container is invalid |
| Repeated calls | Idempotent. Calling it repeatedly on the same container yields only one button, so it's safe to put in every game-over path |
| Appearance | Inherits the container's text color, uses an outlined style, and matches the size of regular buttons — no extra styling needed on your part |
| Label | The button label follows the language the player picked on Yonest (supplied by the platform); you don't need to translate it yourself. |
Languages and accounts
The SDK also provides multi-language and account support: the game ships all its translations in one dictionary table (the platform neither translates nor overrides them); player identity and a login credential are handed to your own backend for verification.
| Item | Description |
|---|---|
| locale | The interface language the player chose on Yonest (e.g. en, de, zh) |
| textLocale | The language actually used to look words up: the current locale when the dictionary covers it, otherwise the source language. Use this (not locale) for the html lang attribute. |
| platformLocales | Every language the platform currently supports (passed in by the shell, read-only array) |
| supportedLocales() | Every locale covered by the dictionary table (in header column order) |
| missingLocales() | Languages the platform supports that this game's dictionary table does not provide yet |
| i18n(table, base?) | Register the game's own dictionary table: row 0 is the header (cell 0 is the key column; each remaining cell declares that column's language), then one row per entry. Pass a JSON array or TSV; the second argument is the source language, defaulting to the first language column in the header |
| t(key, vars?) | Look up a string. Priority: own[selected language] > own[source language] > key |
| applyI18n(root?) | Translate data-i18n / data-i18n-html / data-i18n-attr markers in the DOM (same convention as the main site) |
| setLocale(code) | Switch to a given locale when the game ships its own language menu; switching on the platform side reloads the page and does not go through here |
| onLocaleChange(fn) | Register a locale-change callback that receives the new locale code; returns the SDK object for chaining |
| onReady(fn) | Run fn(Yonest) immediately — only useful once the SDK exists. During init (before install) push the call into window.YonestQueue instead: the SDK replays the queue on install |
| player | Player identity: { id, name, maker }; null when not signed in |
| getToken() | Get a short-lived token scoped to this game, for your backend to verify via /oauth/userinfo and /v1/entitlement/check |
var table = [
// ship the game's own dictionary table — row 0 declares the language of each column
['key', 'zh', 'en', 'de'],
['start', '开始', 'Play', 'Spielen'],
['score', '分数', 'Score', 'Punkte'],
];
function initYonestI18n(Yonest) {
Yonest = Yonest || window.Yonest;
// 2nd arg: source language. Not injected yet? Queue it — replayed on install.
if (!Yonest) { (window.YonestQueue = window.YonestQueue || []).push(initYonestI18n); return; }
Yonest.i18n(table, 'zh');
Yonest.applyI18n(); // translates data-i18n markers in the DOM
// use textLocale (NOT locale) for : it is the language actually rendered
document.documentElement.lang = Yonest.textLocale;
}
initYonestI18n();
// hand a game-scoped token to your own backend for verification
// const { access_token } = await window.Yonest.getToken();
Errors and error codes
Every failure rejects with a YonestError: e.code is a stable code you can branch on, e.message is a localized sentence you can show as-is, and e.retryable tells you whether an identical retry makes sense. Never match on the message text — it is translated.
| code | retryable | When it happens |
|---|---|---|
NOT_LOGGED_IN · GAME_NOT_ACQUIRED | No | The player is not signed in, or has not acquired this game (a paid game that is still locked). Show a hint — do not retry. |
LEADERBOARD_DISABLED · GAME_NOT_FOUND | No | This game has no leaderboard, or it is not published. Do not retry. |
SCORE_OUT_OF_RANGE · INVALID_ARGUMENT | No | Bad input: the score is not a finite number, out of 0–1000000000, or the call itself is malformed. Fix the call; retrying unchanged will fail again. |
RATE_LIMITED · SERVER_ERROR · NETWORK | Yes | Too many calls, a platform-side failure, or the network dropped. Back off (see retryAfter) and retry. |
POINTS_INSUFFICIENT | No | The player's balance cannot cover a paid game. Send them to the points page instead of retrying. |
Domain codes are passed through unchanged, so e.code may be a value that is not in this table. When in doubt, branch on e.retryable and show e.message.
window.Yonest.submitScore(score).catch(function (e) {
if (e.code === 'GAME_NOT_ACQUIRED') { showHint(); return; }
if (e.retryable) { setTimeout(retry, (e.retryAfter || 5) * 1000); return; }
console.warn(e.code, e.message);
});
Local mock and TypeScript types
/yonest-sdk-mock.js mirrors the SDK without the platform: it installs window.Yonest right away, keeps scores and tokens local, and records every call in YonestMock.calls. Use YonestMock.failNext(code) to exercise your error branches.
<script src="./yonest-sdk-mock.js"></script>
<script>
YonestMock.configure({ leaderboard: true, player: { id: 'u1', name: 'Local', maker: false } });
YonestMock.failNext('RATE_LIMITED'); // make the next async call fail on purpose
</script>
/yonest-sdk.d.ts is the typed contract: drop it into your project (or reference it) and window.Yonest, YonestError and window.YonestQueue become typed. Keep it in sync with the build you ship against.
Lifecycle and in-game events
Subscribe with Yonest.on(name, fn) / off(name, fn): pause and resume fire when the tab goes to the background; visibility carries { visible }; fullscreen carries { fullscreen } (forwarded by the shell); beforeLeave fires right before the game page unloads — use it for one last synchronous save.
Yonest.on('pause', pauseTimers);
Yonest.on('resume', resumeTimers);
Yonest.on('beforeLeave', function () { saveProgress(); });
In-game events: Yonest.track(name, result?) counts per game and per UTC day (result is an optional small bucket, e.g. a level id). Events are aggregated — no per-player rows, no free text. track never rejects; it resolves true/false to say whether the report was delivered.
You can download the counts as CSV from the game card in the Maker Console (date,event,result,count; last 30 days by default).
Cloud saves and achievements
Yonest.save.list() / get(key) / set(key, value) / remove(key) keep progress with the account. value is any JSON-serializable value; list() also returns the quota (maxKeys 20, maxBytes 32KB per entry).
// restore on start
Yonest.save.list().then(function (d) {
var slot = d.saves.filter(function (s) { return s.key === 'main'; })[0];
if (slot) state = JSON.parse(slot.value);
});
// save with optimistic locking
Yonest.save.set('main', state, { version: lastVersion })
.then(function (r) { lastVersion = r.version; })
.catch(function (e) { /* SAVE_CONFLICT: merge e.body.current, then retry */ });
Every failure is a YonestError: SAVE_QUOTA (no free keys), SAVE_TOO_LARGE, SAVE_KEY_INVALID. Pass the { version } you last read to enable optimistic locking — a mismatch rejects with SAVE_CONFLICT and e.body.current holds the stored value, so you can merge and retry.
Achievements: Yonest.achievement.unlock(id, name) is idempotent ({ first } tells you whether it was new, so you never show the same toast twice); list() returns what this player already unlocked. Both calls require a signed-in player.
Yonest.achievement.unlock('first_win', 'First win').then(function (r) {
if (r.first) showToast('Achievement unlocked');
});
Leaderboard time windows need no extra work: submitting a score also feeds the daily and weekly boards, bucketed by UTC (daily = the UTC date, weekly = ISO week). The game page switches between all-time, today and this week.
Saves up to 32KB work for every game. Games with at least 200 distinct players in the last 30 days can also write big saves (1MB per entry, 50MB per game) — those come back from list() with backend: "r2" and value: null, so read them with save.get(key).
Adding translations after the platform adds a language
The languages the platform supports come from Yonest.platformLocales (currently en / de / fr / es / ru / ja / ko / zh). To see what your game is missing, run Yonest.missingLocales() inside the game, or read the missing-translation badge on the game's card in the Maker Console.
- Add a column: in your
Yonest.i18n([...])dictionary table, add one column per missing language — the language code in row 0, then the translations below. Cells you are unsure about can stay empty; they fall back to the source language. - Change the version: bump it (e.g.
1.0.0→1.0.1). The update endpoint requires a different version and rejects an unchanged one. - Hand it back: open "Update game" on that game's card in the Maker Console, fill in the slug and the new version, pick the new file and click "Update". The slug does not change.
- Re-check: enter the game again —
Yonest.missingLocales()should return an empty array, and the game page's language list gains the new language.
Do not re-upload under a new name to add translations: that creates a brand-new game and leaves acquires, reviews, leaderboard scores and favourites on the old one. Use "Update game": the slug and all the data stay put.
Score reporting: rules and limits
A submitted score must satisfy all of the conditions below. If any one is unmet, the server returns an error instead of silently dropping it, so you can pinpoint issues while debugging.
| Condition | Error message when unmet |
|---|---|
| Signed in | NOT_LOGGED_IN |
| The game exists and is published | The game does not exist or is not published |
| The game has score reporting enabled | This game doesn't have score reporting enabled yet |
| The player has acquired the game | Get this game before you can submit a score |
| The score is a number between 0 and 1000000000 | The score must be a number between 0 and 1000000000 |
Values are rounded to two decimal places. The leaderboard ranks scores from high to low and counts only each player's best score, so repeated submissions don't lengthen it; improved tells you whether this submission pushed the record higher.
Tell us when you need the leaderboard enabled and the platform will mark this game as integrated; games without it show no leaderboard entry and don't get submitScore.
Moving your game to another platform
There are three paths — pick whichever is easiest for your situation.
| Approach | How | When to use |
|---|---|---|
| Take the file as-is | Download the HTML from the platform. It never contained platform code, so it runs standalone anywhere | You just want to move the game over unchanged; the if (window.Yonest) checks skip themselves elsewhere |
| Delete the marked blocks | Search for yonest:start and delete everything between each pair of markers |
You want no platform-related lines left in the code at all |
| Export with one-click stripping | Enter your slug on the Publish page and click “Download portable version”; the platform returns the stripped file and tells you how many hook calls remain | You want a version that's confirmed clean, or need to hand it to a partner |
How to verify
Search the exported file for Yonest — the result should be 0. Then open it on its own: the game should start and finish normally, with no score notices or back buttons. The platform also returns two headers with the export, X-Yonest-Hooks-Original for the number of hook calls before stripping and X-Yonest-Hooks after; if the two numbers match, everything was removed.
If you want to own the shell yourself
The SDK depends entirely on the single global window.Yonest and has no other coupling. You can implement an object with the same name to take over its behavior — for example, point back() at your own page, or use an empty implementation on another platform so every call becomes a no-op.
FAQ
Will opening this HTML locally throw errors?
No. Every platform call is inside if (window.Yonest); when the object doesn't exist locally the calls are skipped, and the rest of the game is unaffected.
Why doesn't my game have window.Yonest.submitScore?
It means this game doesn't have the leaderboard enabled. back(), gameOverActions() and the like are still available, but submitScore doesn't exist and calling it directly throws; check window.Yonest.supportsLeaderboard first, then call.
Is submitting multiple scores in one run a problem?
It won't affect correctness, but each submission pops a notice in the top bar. For mid-run reporting, add { silent: true } and use the default behavior only at the real end.
Will the game-over button be inserted more than once?
No. gameOverActions() is idempotent and keeps only one button per container, so it's safe to put in every game-over path.
Will a failed submission interrupt my game?
No. Submission returns a Promise; as long as you add .catch() as shown, a failure is just a swallowed error and the game moves on to the next round. This covers cases like the player not having acquired the game or an expired session.
Will the SDK slow down loading?
It isn't added to your file and it isn't injected into your document.
Can I hide the top bar entirely?
You can collapse the secondary buttons (setChromeVisible(false)), but the back entry point is kept — this avoids leaving players with no way out. If your gameplay genuinely needs a full-screen area, tell us and we'll discuss the full-screen case.
Version and download
The SDK file is provided and loaded by the platform; you never need to put it into your own HTML. If you want a copy saved locally for debugging, you can download one from the Publish page.
If you hit problems during integration, write to hello@yonest.games, or leave a note via the Publish page.