Read this page as markdown — for coding agents, or for anyone who prefers the source.
Arcade SDK
Reference for public/sdk/v0/bananai-sdk.js, served at
/sdk/v0/bananai-sdk.js. Current version: 0.2.0.
Every game must load the SDK and use it for all communication with the portal.
Games run in a sandboxed iframe and must never touch window.parent directly.
A note on prior art
The API surface here deliberately follows the shape that portal SDKs have converged on — a loading phase, explicit gameplay start/stop, a game-over signal, and ad hooks. That convention is worth adopting for a concrete reason: a developer who has shipped on another portal can port a game here in an afternoon.
The implementation and this documentation are our own. We do not copy another portal's SDK source or docs — that is someone else's copyrighted work, and shipping it would be both a legal problem and an obstacle to the independence we actually want. Following an established interface convention is normal engineering practice; copying an implementation is not.
Advertising is part of that convention and works here for real as of 0.1.1.
commercialBreak() and rewardedBreak() were shims that resolved immediately in
0.1.0; they now cross to the shell, which owns the entire ad stack. A game
written against an ad-funded portal runs here unmodified either way, which was
the point of matching the shape in the first place.
Your game never loads an ad script. It asks; the shell answers. That is what keeps your folder self-contained, keeps third-party ad code off your origin, and lets the portal change ad providers without you redeploying anything.
Wire protocol
One envelope, as of 0.2.0:
// game -> shell
{ source: 'bananai-sdk', sdkVersion: '0.2.0', gameId: 'gemburst', type, payload }
// shell -> game
{ source: 'bananai-shell', type, payload }
Replies are typed <type>:reply.
Three reasons this is the right shape, all of which the previous one lacked:
gameIdtravels with every message. The host would otherwise have to infer which game is talking from which frame sent the message, which stops working the moment more than one frame exists.sdkVersionis declared. The host can adapt to older games instead of guessing, which is what makes supporting third-party submissions realistic.- Both ends are named.
sourcesays who sent it; a sharedchannelconstant only says the message belongs to us, not which direction it travels.
Until 0.2.0 the SDK sent { channel: 'turbigo', type, payload } and the host
accepted both shapes, replying in whichever it was addressed in. That
compatibility layer is gone: it was carrying the old product name, and an alias
kept for a name nothing uses is an alias that never gets removed.
If you have a copy older than 0.2.0, it can no longer talk to the portal.
Nothing partially works — the handshake times out, and your game runs as though
it were standalone: no loading bar, no score record, no playtime attribution,
and bananai:restart never arrives. Take a fresh copy of the file.
Lifecycle
await Bananai.init({ gameId: 'gemburst' });
// while loading assets
Bananai.loadingProgress(0.4);
Bananai.loadingFinished();
// when the player actually starts playing
Bananai.gameplayStart();
// on pause, menu, or any interruption
Bananai.gameplayStop();
// when a run ends
Bananai.gameOver({ score: 12500, level: 7, durationMs: 84000 });
API
init(options): Promise<void>
Must be awaited before any other call. Resolves once the shell acknowledges, or immediately when the game runs standalone.
| Option | Type | Description |
|---|---|---|
gameId |
string | Required. Must match the catalogue ID. |
debug |
boolean | Logs SDK activity to the console. |
loadingProgress(fraction)
fraction is clamped to 0..1. Drives the shell's loading indicator. Call it
during asset loading, not after.
loadingFinished()
Assets are ready. The shell hides its loading state.
gameplayStart() / gameplayStop()
Bracket every period of active play. Idempotent — repeated calls in the same state are ignored. The shell uses these to avoid interrupting live gameplay and to measure engagement.
These two calls are how you get paid. Active playtime is the basis of the revenue share, and a game that never brackets its play looks to the portal like a game nobody plays. They are also what stops an ad appearing over a live run. See the revenue share terms, sent on request.
Call gameplayStop() on pause, on opening a menu, and whenever the tab loses
visibility.
gameOver(result)
Ends the run and produces a score record. Implicitly calls gameplayStop().
| Field | Type | Required |
|---|---|---|
score |
number | yes |
level |
number | no, defaults to 1 |
durationMs |
number | recommended — used for plausibility checks |
happyMoment(intensity)
Signals player delight: a cleared level, a beaten personal best. Telemetry only today; the shell may later use it to time non-intrusive prompts. Do not call it on every point scored.
commercialBreak(): Promise<void>
Asks for a commercial break and resolves when it is over.
Call it between activities only — a menu, a level transition, a game over —
and call gameplayStop() first. Your game is suspended for as long as the
promise is pending, and the shell refuses the break outright if its own gameplay
signals say a run is still live.
It always resolves, and resolves quickly when there is nothing to show: no fill, frequency caps, no consent, advertising disabled, a blocked ad script. Nothing in your game may depend on a break having actually happened.
rewardedBreak(): Promise<boolean>
Plays a rewarded video and resolves true only if it was watched to the end.
Ask the player first — the offer is yours, since only your game knows what it is offering. The shell just plays the video.
Games must handle false gracefully, and false is the common case: no
fill, a dismissal, no consent, an ad blocker, or a portal with advertising
switched off. A game that gates progress behind a rewarded video is not
publishable here.
Bananai.gameplayStop();
const earned = await Bananai.rewardedBreak();
if (earned) grantExtraLife();
else showNormalGameOver(); // must be a complete experience on its own
Bananai.gameplayStart();
Rewarded pays more per impression than any other format, so a natural offer — a continue, an extra life, a hint — is worth implementing. How that turns into money is in the revenue share terms, sent on request.
setDebug(enabled)
Events from the shell
The SDK re-dispatches shell messages as CustomEvent on window:
window.addEventListener('bananai:restart', () => resetGame());
window.addEventListener('bananai:pause', () => pauseGame());
window.addEventListener('bananai:resume', () => resumeGame());
bananai:restart fires when the player chooses to play again after a game over.
Handling it is mandatory — a game that requires a page reload to restart fails
QA.
Standalone mode
When a game is opened directly rather than embedded, window.parent === window
and every SDK call becomes a safe no-op. Games must remain fully playable in
this mode; it is how developers work locally and how QA reviews submissions.
Migration from the v0 event contract
Gem Burst predates the SDK and uses raw window events (arcade:gameover,
arcade:restart). Those still work but are deprecated.
| v0 | v0.1 |
|---|---|
dispatchEvent('arcade:gameover', {score, level}) |
Bananai.gameOver({score, level}) |
addEventListener('arcade:restart') |
addEventListener('bananai:restart') |
Gem Burst gets migrated in phase 2, alongside score persistence.
Loading the SDK
Two supported options. Both are accepted in review, but they are not equally good, so this section says which to prefer and why.
Preferred: link to the portal's copy
<script src="/sdk/v0/bananai-sdk.js"></script>
Your game is served from the portal's own origin, under /games/<id>/, so this
path resolves without any configuration. There is one copy of the SDK on the
whole site, and fixes reach your game without you redeploying — which is the
entire point of the versioning policy below.
Accepted: vendor a copy into your folder
<script src="js/vendor/bananai-sdk.js"></script>
The folder stays self-contained and portable to another portal. The cost is that
the wire protocol freezes at the version you copied, including the
advertising methods. Declare the exact version in bananai.json under
sdkVersion so review can tell what your game is actually running.
Vendor when you have a reason to — you ship the same build to several portals, or you need the game to run from a folder that is not under a portal root. Link otherwise.
Never write the origin
<script src="/sdk/v0/bananai-sdk.js"></script> <!-- correct -->
<script src="https://bananai.games/sdk/v0/bananai-sdk.js"></script> <!-- rejected -->
The absolute form looks more explicit and is strictly worse. It is a
cross-origin request everywhere the portal is not live production: on your
machine, on the preview domain, and in npm run check, which fails any request
leaving the origin under test. It also stops working the day the portal changes
domain — and a game folder is not ours to edit, so we could not fix it for you.
The root-relative form is correct in all of those places at once, and
npm run check rejects the absolute one.
If you develop with your game folder as the document root, /sdk/v0/... will
404 locally. Put the file at <your-root>/sdk/v0/bananai-sdk.js while you work:
the path is then identical to production, and what you test is what ships.
Vendor 0.2.0, and download the file rather than writing your own. Both halves of that matter:
- Anything older than 0.2.0 speaks the previous envelope and cannot reach the portal at all. Before that, 0.1.0 had a second defect worth knowing about: its two advertising methods were local stubs that resolved without sending anything, so a game vendoring it could not show an ad or earn from one, and nothing about it looked broken — it simply never requested. One game in our own catalogue is in exactly that state, and it is the reason this paragraph exists.
- A reimplementation written from this document will be subtly wrong in ways
neither of us will enjoy diagnosing: the envelope, the reply types, the
timeouts and the duplicate-suppression rules are all part of the contract, and
prose is not a specification. Take the file from
/sdk/v0/bananai-sdk.jsand ship that.
Whichever you choose, sdkVersion must state what the package actually
contains. Declaring a version you have not shipped is worse than shipping an
old one.
Versioning
The SDK follows semver. Games pin a major version:
<script src="/sdk/v0/bananai-sdk.js"></script>
Breaking changes ship as a new major with the old one kept alive for at least six months. Third-party developers cannot be expected to redeploy on our schedule.
0.2.0 is a breaking change that did not get a new major, and here is why.
The rule above protects deployed games. There were none to protect: the file was
documented at /sdk/v0/ while it actually sat at /sdk/, so the URL this
document has always given returned 404 for its entire existence — confirmed by
an external developer, who reimplemented the SDK from prose because they could
not download it. Nothing was ever served from /sdk/v0/, so nothing could break
by changing what lives there. Minting /sdk/v1/ to preserve a /sdk/v0/ that
never existed would document a history that did not happen.
The next breaking change will not have that excuse, and gets /sdk/v1/.
0.2.0 unified the envelope and carried the rename from the product's previous name. Both were breaking and both were unavoidable, so they went out together rather than asking every developer to migrate twice for one product decision. 0.1.1 turned the two advertising methods from stubs into real calls; 0.1.0 was the first release.
Two older URLs still redirect here rather than 404, because a game folder is not
ours to edit and neither is anyone else's <script src>:
/sdk/turbigo-sdk.js → /sdk/v0/bananai-sdk.js
/sdk/v0/turbigo-sdk.js → /sdk/v0/bananai-sdk.js
A redirect fixes the URL, not the file behind it: a game that follows one still receives 0.2.0 and must speak the current envelope.