Guides
A game without a backend
Build a browser game that sells a currency pack and an item with only the browser SDK and a publishable key, from first line to a working page.
On this page
What you will build
A page where a player buys gems with euros (simulated in test), buys a potion with gems, and drinks it. There is no server of your own: the game talks to GameCoin straight from the browser, with the SDK.
This is the path for a game on itch.io, in a game jam, in a no-code engine, or written with an AI assistant. If your game has its own server, read A game with a backend instead.
Before you start
You need a game and a catalog. The dashboard is in French for now.
- Create an account and a game (« Nouveau jeu »). The two test keys are created with it.
- Create the paid currency
gemsin « Monnaies » (type « Payante »). - Create the item
potionin « Objets »: type « Consommable », « Prix en monnaie » 20 ingems, and tick « Achetable depuis le jeu (API cliente) ». Without that box the browser cannot buy it. - Create the pack
gems-500in « Packs »:gemswith « Quantité payée » 500 and « Quantité offerte » 50, « Prix (en euros) » 4,99. A new pack is a draft: use « Publier » so players can see it. - In « Clés d'API », copy your publishable key
gc_pk_test_….
The examples use these codes. Use your own codes where yours differ.
| What | Code | Content |
|---|---|---|
| Paid currency | gems | |
| Item | potion | Consumable, 20 gems, buyable from the game |
| Pack | gems-500 | 500 gems + 50 free gems, 4.99 € |
Leave « Autoriser les joueurs anonymes » on in the game's settings (« Réglages »). It is on by default, and this path needs it.
Step 1: Load the SDK and start it
The SDK is one file with no dependency and no build step. It adds a global GameCoin and talks to the host it was loaded from.
<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script type="module">
const gc = await GameCoin.init({ publishableKey: "gc_pk_test_…" });
console.log(gc.player.id); // the anonymous player, remembered in this browser
console.log(gc.balance("gems")); // 0: the player has bought nothing yet
</script>init does the work of a login for you. On the first visit it creates an anonymous player and keeps its secret in localStorage. On the next visits it asks for a fresh token with that secret. The same browser is always the same player, with the same balance. Clearing the browser's data makes a new player.
Note
Open the page from a web server (a local one is fine), not from a file:// address. Itch.io and most engines already serve your game over https://.
Step 2: Show the shop from the catalog
Do not copy prices into your code. Ask the catalog: it holds only the active currencies, items and packs.
const catalog = await gc.getCatalog();
console.log(catalog.packs[0]);{
"sku": "gems-500",
"name": "Bag of gems",
"description": null,
"priceCents": 499,
"currency": "EUR",
"grants": [{ "currency": "gems", "amount": 500, "bonus": 50 }],
"items": [],
"badge": null,
"maxPerPlayer": null
}A pack's price is in euro cents, VAT included: 499 is 4.99 €. Divide by 100 to display it.
Step 3: Keep the screen in sync
The SDK keeps a copy of the player's wallet and inventory. gc.balance("gems") and gc.owned("potion") read it with no request. The change event fires whenever the copy changes, after a spend, a purchase, a consume, a payment or a refresh. Draw your numbers in one function and call it from the event:
const render = () => {
document.getElementById("gems").textContent = gc.balance("gems");
document.getElementById("potions").textContent = gc.owned("potion");
};
gc.on("change", render);
render();Step 4: Sell a pack
checkout sells a pack for euros. It opens the payment page in a window and finishes when the order is over. Call it straight from a click, or the browser blocks the window.
document.getElementById("buy-pack").onclick = async () => {
const order = await gc.checkout("gems-500");
console.log(order.status); // "fulfilled", "failed" or "pending"
};order.status | What happened | What to do |
|---|---|---|
fulfilled | The player paid. The gems are in, and gc.balance("gems") is already up to date. | Thank the player |
failed | The payment was declined | Offer to try again |
pending | The player closed the window without paying | Nothing: the order expires by itself after an hour |
In test, the payment page is simulated: it shows the order, the VAT and two buttons, « Payer » and « Refuser ». No money moves. The page is in French for now.
If the browser blocks the window, the SDK sends the current tab to the payment page instead, and brings the player back to the same URL. You can force that with gc.checkout("gems-500", { mode: "redirect" }). When the page loads again, init reads the order, removes ?order=…&status=… from the address, and exposes the order as gc.returnedOrder. See Browser SDK.
Step 5: Spend, buy an item, consume it
Three calls change a balance downwards:
| Call | Use it for |
|---|---|
gc.spend("gems", 5, "continue") | A cost with no item: "continue?", a skip, an entry fee |
gc.buyItem("potion", 1) | Buy an item with its own price, in its own currency |
gc.consume("potion", 1) | Use up a consumable the player owns |
Each one can fail with INSUFFICIENT_FUNDS, and that is a normal event, not a bug. Its details say how short the player is:
async function run(action) {
try {
await action();
} catch (error) {
if (error.code === "INSUFFICIENT_FUNDS") {
const { required, available } = error.details;
say(`You need ${required - available} more.`); // send the player to the shop
} else {
say(`Something went wrong (${error.code}).`);
}
}
}The whole page
All the steps together. Put your own publishable key in place of gc_pk_test_….
<h1>Potion shop</h1>
<p>Gems: <strong id="gems">0</strong> · Potions: <strong id="potions">0</strong></p>
<button id="buy-pack">Buy 500 gems</button>
<button id="buy-potion">Buy a potion (20 gems)</button>
<button id="drink">Drink a potion</button>
<p id="message" role="status"></p>
<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script type="module">
const gc = await GameCoin.init({ publishableKey: "gc_pk_test_…" });
const $ = (id) => document.getElementById(id);
const say = (text) => ($("message").textContent = text);
// The screen follows the SDK's copy of the wallet and the inventory.
const render = () => {
$("gems").textContent = gc.balance("gems");
$("potions").textContent = gc.owned("potion");
};
gc.on("change", render);
render();
// Run a call and turn its errors into messages.
async function run(action) {
try {
await action();
say("Done.");
} catch (error) {
if (error.code === "INSUFFICIENT_FUNDS") {
say(`You need ${error.details.required - error.details.available} more gems: buy a pack.`);
} else {
say(`Something went wrong (${error.code}).`);
}
}
}
// Pack paid in euros. Called straight from the click: the payment window needs it.
$("buy-pack").onclick = () =>
run(async () => {
const order = await gc.checkout("gems-500");
say(order.status === "fulfilled" ? "Thank you!" : `Payment ${order.status}.`);
});
// Item paid with the game's currency, then used up: no window, no euros.
$("buy-potion").onclick = () => run(() => gc.buyItem("potion"));
$("drink").onclick = () => run(() => gc.consume("potion"));
</script>Try it: the buttons for the potion fail at first, because the player has no gems. Buy the pack, pay in the simulated page, and the gems appear. Then buy a potion and drink it. The balance and the potion count update on their own.
What a player cannot do
The SDK has no grant, and neither does the API it calls with a publishable key. A player can open the developer tools and call anything the SDK can, but that is only spending their own gems or paying for a pack. They can never add coins to their balance, except the free coins of a gift code that you issued (one use per player). That is why this path is safe for real money. Security model explains it.
It also means a game without a backend cannot give gems as a reward. Points that your game computes locally, such as a score, stay in your game. If you want rewards in currency, add a server: A game with a backend.
If something goes wrong
| You see | Why | Fix |
|---|---|---|
publishableKey must be a publishable key | The key does not start with gc_pk_ | Copy the publishable key, not the secret one. The SDK refuses a gc_sk_ key on purpose. |
baseUrl is required when the SDK is not loaded from a <script src> | The SDK was bundled or imported, so it cannot tell where GameCoin is | Pass baseUrl: "https://gamecoin.apilow.com" to init |
FORBIDDEN on init | Anonymous players are turned off for this game | Turn them back on in the settings, or use a backend |
FORBIDDEN on buyItem | The item is not « Achetable depuis le jeu » | Tick the box on the item |
NOT_FOUND on checkout | The pack code is wrong, or the pack is still a draft | Publish the pack and check its sku |
| The payment window does not open | The click was not the direct cause of checkout | Call checkout from the click handler, with no await before it |
Where next
- Browser SDK: every option and call.
- Selling packs: VAT, return URLs and the life of an order.
- Testing: the test environment, test players and the demo game.