Game SDK
Games talk to the arcade through window.taploka. The host injects the SDK into your index.html when it serves the
bundle, so your bundle must not ship it. Just call it.
Why it is a bridge, not an HTTP client
The game runs in a sandboxed frame on an opaque origin: no cookies, no storage, no way to reach the API on its own. Every call is a message to the host page, which performs the request with the player's session and sends the result back. This is what stops a bundle from acting as the player outside the game.
Practical consequences:
localStorageandsessionStorageare unavailable. Usesave()/load()instead.- Relative fetches to the arcade will not authenticate, because there is no session in the frame.
- Everything the game needs must be inside the bundle.
API
Each method returns a promise.
ready()
const { game, version, player, studio, sdk, room } = await taploka.ready();
Call it once at start-up. It tells you which game and version you are running as, and who is playing. room is the
code of a room the player tapped on the game's page in the arcade, or null on a normal launch; see
Multiplayer rooms.
version is the bundle version number the player was served, the same one the console lists, so a title screen or a
bug report can print it instead of keeping its own build string. studio is the credit the shelf shows for the game,
which is the developer's studio name, or the arcade itself for a game it published on its own. Print those two rather
than hardcoding them: a hardcoded version goes stale on the next push, and a hardcoded studio goes stale the moment a
game changes hands. Both are null when no host answered, so keep a fallback for a standalone run.
submitScore(score, meta?)
const { accepted, rank, best } = await taploka.submitScore(1200, { level: 4 });
The score is floored to a non-negative integer. accepted is false when the score is held for review. A game should
keep playing normally either way. A game may declare a maximum plausible score; anything above it is flagged rather
than published to the leaderboard.
A game that lets players name themselves, a company, a crew, a stable, sends that name as meta.name:
await taploka.submitScore(1200, { name: 'PT Maju Jaya', level: 4 });
It becomes the name on this game's board, in the game and on the arcade's own leaderboard page, and it changes nothing about the account. Send it with every submission: a rename lands even when the score is not a new best. The text is trimmed of control characters and cut to 24 characters. Leave it out and the account name stands, which is what a game without its own names should do.
save(payload) and load()
await taploka.save({ level: 3, coins: 120 });
const state = await taploka.load(); // parsed object, or null on a fresh player
Cloud save is per player and per game. Objects are serialised for you; strings are stored as given. save() returns a
revision number.
leaderboard(limit?, period?)
const { entries, userRank, period } = await taploka.leaderboard(20);
const today = await taploka.leaderboard(20, 'daily');
// entries: [{ rank, name, score, meta, isCurrentUser }]
Read the display name from name; a player without one comes back as "Player". userRank is the current player's
position, if they have one. around holds the player's own row and up to five either side of it, in the same shape as
entries, so a game can show or match the people near a player who sits below the page it asked for. It is empty for a
player with no row yet, and it can repeat rows that are also in entries.
Every score is written to three boards at once, and period picks which one comes back: daily (resets at midnight WIB,
the same day boundary as quests and streaks), weekly (the one that pays coins when it closes) or alltime. Leave it
out and it is the weekly. periodKey comes back beside it, so a game can tell a stale board from a fresh one after the
day flips.
meta is the object the game sent with submitScore for the run that won that row, or null when it sent none, so a
board row can show more than a number: a fleet, a level reached, a company's revenue. It is the game's own payload
coming back, not something the arcade fills in. It is replaced together with the score, so it always belongs to the
score next to it.
wallet(), spend(amount, reason, options?)
const { balance, earned, spent } = await taploka.wallet();
const after = await taploka.spend(50, 'continue', { key: `continue-${runId}` });
Coins are the arcade's currency and follow the player across games. spend() deducts and grants nothing, so use it for
consumption your shop cannot express. It rejects with error.code === 'INSUFFICIENT_BALANCE' when the player is short.
shop(), buy(sku, options?), inventory()
const { items } = await taploka.shop(); // [{ sku, title, coin_price, type, owned, qty }]
const { item, balance } = await taploka.buy('engine_lv2');
const owned = await taploka.inventory(); // [{ sku, qty, granted_at }]
The price comes from a list you register per game, so a bundle can only name a sku. Ownership is stored server-side:
read inventory() at start-up instead of trusting your cloud save, which the client writes and can edit. Buys are
idempotent, and buy() rejects with ITEM_OWNED, ITEM_UNKNOWN or INSUFFICIENT_BALANCE.
See Coins and shop for the whole flow, including how to register the price list.
prize.status(), prize.claim()
const { rank, coins, claimed, prizes, balance } = await taploka.prize.status();
const { paid } = await taploka.prize.claim(); // today's prize, once
Today's coin prize for a top seat on this game's daily board. The seat is read on the server, from the leaderboard the
arcade keeps, so a game can ask to be paid but never say what it is owed. coins is what this player's seat is worth
right now, and rank is null for somebody who has not scored today.
prizes is the table itself, keyed by the last seat of each tier, so { "1": 100, "2": 50, "3": 25, "10": 10 } pays
the winner 100 and seats 4 to 10 ten coins each. Show it before anyone claims: it is the reason to play one more round.
One prize per player per day. claim() pays once and a second tap answers paid: 0 with claimed: true. The day also
settles by itself an hour before midnight, arcade time, so a game with no claim button still rewards its players, and
claiming from inside the game only takes the same coins earlier. Account players only, so a guest gets
ACCOUNT_REQUIRED.
market.list(), market.sell(item, price), market.buy(id), market.cancel(id), market.claim()
const { listings, proceeds } = await taploka.market.list(); // [{ id, price, item, mine, seller, listed_at }]
const { listing } = await taploka.market.sell({ truck: 'hino', wear: 12 }, 7500000);
const { item } = await taploka.market.buy(listings[0].id); // the item, handed over once
await taploka.market.cancel(listing.id); // takes it down, item comes back
const { total, sales } = await taploka.market.claim(); // what sold while you were away
A stall players sell to each other from, in your game's own money, not arcade coins. The item is your own shape
and comes back to the buyer exactly as you listed it. While a listing is open the arcade holds the item, which is what
stops the same truck being sold twice: buy() locks the row, so of two buyers pressing together one gets the item and
the other gets LISTING_GONE.
Two things follow from where the money lives. A seller is usually away when their thing sells, so the price waits on
the server until their game calls claim(); claiming twice pays once. And the arcade never checks the trade itself:
your balance is in your save, which the client writes, so "the seller owned this" and "the buyer could afford it" are
claims your game makes, not facts the server can verify. Deduct on the buyer, pay on the claim, and keep prices inside
a band you decide.
Account players only, so ACCOUNT_REQUIRED is the refusal a guest gets. Twelve open listings per player per game.
feed.on(listener), feed.send(payload)
taploka.feed.on((line) => {
if (line.payload.type !== 'jackpot') return;
marquee(`${line.from} won ${line.payload.prize}`); // line.from is escaped by you
});
await taploka.feed.send({ type: 'jackpot', prize: 'gold' });
A room reaches the people you are playing with. The feed reaches everybody playing this game, in a room or not,
which is what a rare drop, a jackpot or a broken record wants. line is { from, payload, at }.
from is the sender's display name, taken from the account by the server. A game cannot set it and a player cannot
forge it, which matters here more than in a room, because this channel is public: anyone who can open the game can
read every line on it. Put nothing in payload you would not put on a billboard, and never a save, a token or a
score the server has not already seen. At most 512 bytes and 12 keys.
The socket opens on the first on() and on nothing else, so a game that never announces anything costs nothing.
send() resolves whether or not anybody is listening; there is no history, a player who was offline missed it. Six
lines per minute per player, and a host older than this API has no taploka.feed at all, so check for it and fall
back to the room:
const wide = taploka.feed && typeof taploka.feed.send === 'function';
track(name, params?)
taploka.track('level_end', { level: 4, outcome: 'win' });
A gameplay event for analytics. Fire and forget.
exit()
taploka.exit();
Ask the host to close the game and return the player to the catalog. Call it from your own quit button, not on a lost life.
Minimal example
<script>
(async () => {
const session = await taploka.ready();
const state = await taploka.load() ?? { level: 1 };
startGame(state, {
onLevelEnd: (level) => taploka.track('level_end', { level }),
onGameOver: async (score) => {
await taploka.submitScore(score);
await taploka.save({ level: state.level });
},
});
})();
</script>
Astrocade lib bundles
Bundles built against the Astrocade host call a global window.lib. The arcade installs an adapter that maps lib.*
onto the same bridge, so those bundles run unchanged, including the leaderboard calls, which reach the real server
instead of the playtest shim they shipped with.
Checklist before pushing
- The game starts from
index.htmlwith no build step at runtime. - No absolute URLs to your dev machine; all assets are in the bundle.
sdk_versioningame.jsonmatches the SDK you targeted.- Scores are submitted at game over, not on every frame.
- The game survives
load()returningnull. - Anything sold for coins is read back from
inventory(), not from the cloud save.