Coins and the in-game shop
Coins are the arcade's currency. One balance follows the player across every game, so what someone earns in a racing game they can spend in a puzzle. Your game does not mint or hold coins; it asks the arcade to move them.
Where a player's coins come from
| Source | Rule |
|---|---|
| Quests | Daily, weekly and event missions that are not about playing (a rewarded ad, for example), claimed by the player |
| Daily reward | A streak, growing for consecutive days |
| Rewarded ads | Credited after the ad network verifies the view |
| Leaderboard | Paid out when the weekly board closes, plus the daily rank prize claimed in the game |
| Purchase | Coin packs bought with money in the first-party app |
Nothing your game does grants coins, and playing pays none either: from a game, coins reach a player only through its leaderboard. A high score does not pay out directly; it pays when it ranks.
Selling something for coins
Two steps: register the price list, then call buy().
1. Register the catalogue
The price lives on the server, never in your bundle. Declare the whole list with the MCP server:
update_game(game: 'rosok-garage', shop: [
{ sku: 'engine_lv2', title: 'Mesin Lv2', coin_price: 800 },
{ sku: 'paint_gold', title: 'Cat emas', coin_price: 400 },
{ sku: 'fuel', title: 'Bensin', coin_price: 150, type: 'consumable' },
])
or with the Harness API if you are scripting it yourself.
shop replaces the list wholesale. A sku you leave out is deactivated rather than deleted, so a player who already
bought it keeps a name to show. Fields:
| Field | Meaning |
|---|---|
sku |
Item id, letters, digits, _ and -, unique within the game |
title |
What the player sees in their wallet history |
coin_price |
Cost in coins, at least 1 |
type |
non_consumable (owned once, the default) or consumable (stacks a quantity) |
active |
Set false to retire an item without dropping it from the list |
2. Buy from the game
const { items } = await taploka.shop();
// [{ sku: 'engine_lv2', title: 'Mesin Lv2', coin_price: 800, type, owned: false, qty: 0 }]
try {
const { item, balance } = await taploka.buy('engine_lv2');
unlockEngine(item.sku);
showBalance(balance);
} catch (error) {
if (error.code === 'INSUFFICIENT_BALANCE') {
showTopUpHint();
}
}
You send a sku and nothing else. The arcade reads the price from the catalogue, charges the wallet and records the item, all in one transaction: a failed charge grants nothing, and a granted item was definitely paid for.
3. Read what they own on start-up
const { items } = await taploka.inventory(); // [{ sku, qty, granted_at }]
Do this instead of trusting your cloud save. A save is written by the client and can be edited; the inventory cannot. Treat the save for progress, the inventory for entitlements.
Errors worth handling
error.code |
When |
|---|---|
INSUFFICIENT_BALANCE |
Not enough coins. Offer the arcade wallet, do not fake the purchase |
ITEM_OWNED |
A non-consumable they already have. Refresh your inventory |
ITEM_UNKNOWN |
The sku is not in the catalogue, or is deactivated |
Retries and double taps
Every buy is idempotent. The host attaches a key for you, so a request that times out and is retried is charged once. If your own code may fire the same purchase twice, pass a stable key of your own:
await taploka.buy('fuel', { key: `fuel-${runId}` });
Two buys with the same key are one charge. A non-consumable is refused with ITEM_OWNED regardless of the key, so an
impatient double tap can never pay twice.
Spending without a catalogue item
For a charge that is not a listed item, deduct directly:
const { balance } = await taploka.spend(50, 'continue', { key: `continue-${runId}` });
reason is a short slug the wallet history shows. Use this for consumption a shop entry cannot express, like paying to
continue a run. It grants nothing, so whatever the player bought is yours to remember. Prefer buy() when it fits:
only buy() gets a server-side price and a tamper-proof record of ownership.
Reading the balance
const { balance, earned, spent } = await taploka.wallet();
Show it if your shop has a screen. Do not cache it across a session: quests and rewards can credit the player while they are playing.
What you earn, and cashing it out
Every coin a player spends in your game pays you a share, 70% by default, credited to your wallet as diamonds in the same transaction as the sale, so it is there before the player's screen has finished animating. Buying from your own game pays nothing: the share would come straight back and the item would be free.
Your developer panel shows it under Earnings: what each game has paid you, what is ready to cash out, and the rate.
1.000 coin item → 700 diamonds to you → Rp1.400 at Rp2 per diamond
Cashing out is a request an administrator settles by hand. There is no automatic transfer:
- You ask for an amount, at least the minimum (50.000 diamonds by default), with a bank or e-wallet account.
- The diamonds leave your wallet immediately. That hold is what stops the same balance being requested twice.
- An admin makes the transfer and marks it paid, or rejects it. A rejection puts the diamonds straight back.
Two rules worth knowing before you plan around it:
- Only diamonds are cashable. Coins you bought, won from a quest or a leaderboard are yours to spend, never to sell back. The arcade buys back what it paid you, and nothing else.
- Diamonds are not coins. They sit in their own balance next to your coins, and no game takes them: they can only be cashed out or fund a clip campaign. Moderator pay and TikTok clip pay arrive as diamonds too.
The rate is set by the arcade and copied onto each request when you make it, so a change never re-prices a payout that is already waiting.
Guests
A player who has not signed in still earns and spends normally, and everything they bought moves onto their account the moment they register. Do not gate a shop behind signing in. What guests cannot do is enter the leaderboard or buy coin packs with money.