Skip to content
Skip the menu

Guides

A game with a backend

Run GameCoin from your own server, reward players there, hand each browser a one-hour token, and use the browser SDK for the rest.

View as Markdown

On this page

What you will build

Your server is the authority. It knows its players, decides what they earn, and credits them. The browser gets a short-lived token and uses the SDK to show the balance, spend, sell packs and use items. In this guide you will:

  1. Install the server SDK and make a client.
  2. Reward a player from your server.
  3. Give the browser a token.
  4. Start the browser SDK with that token.
  5. Let the server spend when the server must decide.

If your game has no server, read A game without a backend instead. The catalog set-up (a gems currency, a potion item, a gems-500 pack) is described there.

Step 1: Install and connect

You need Node.js 18 or later for the Node.js examples. Every example also has a curl version, and the other server languages are in Server SDKs.

Bash
npm install @apilow/gamecoin

Put the secret key of your game (the gc_sk_test_… key from « Clés d'API » in the dashboard, which is in French) in the environment of your server. It never goes in your code or in the browser.

Bash
export GAMECOIN_SECRET_KEY="gc_sk_test_…"
gamecoin.mjs
import { GameCoin, ext } from "@apilow/gamecoin";

export const gamecoin = new GameCoin(process.env.GAMECOIN_SECRET_KEY, { baseUrl: "https://gamecoin.apilow.com" });
export { ext };

baseUrl is where GameCoin runs. The SDK adds /api/v1. The other examples on this page use gamecoin and ext from this file. The .mjs extension makes Node.js read the files as modules without any other set-up.

Step 2: Name your players

A player is your own id behind ext:. You do not need to declare players first: the first write for an id creates it. When you do want to store a name or a country, send the profile. The id is URL-encoded exactly once; the SDK's ext() and the %3A below do that.

LangageLanguage
curl -X PUT "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"displayName":"Player 42","country":"BE"}'
Answer
{
  "id": "6ac4199eb5efc66d69f4d301",
  "externalId": "user-42",
  "kind": "external",
  "displayName": "Player 42",
  "country": "BE",
  "blocked": false,
  "createdAt": "2026-10-05T21:41:50.328Z"
}

Step 3: Reward a player from your server

Rewards are yours to give: a level, a quest, a daily bonus. Your server checks that the event really happened, then grants. A grant always credits the free (bonus) part of the balance.

Build the idempotency key from the event, so that retrying the same event can never pay twice: level-3-user-42, not a random value.

LangageLanguage
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet/grant" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: level-3-user-42" \
  -H "Content-Type: application/json" \
  -d '{"currency":"gems","amount":100,"reason":"level-3"}'
Answer
{
  "entry": {
    "id": "6ac4199e37c701028804249c",
    "type": "grant",
    "currency": "gems",
    "paidDelta": 0,
    "bonusDelta": 100,
    "balanceAfter": { "paid": 0, "bonus": 100, "total": 100 },
    "reason": "level-3",
    "metadata": {},
    "ref": {},
    "createdAt": "2026-10-05T21:41:50.594Z"
  },
  "balance": { "currency": "gems", "paid": 0, "bonus": 100, "total": 100 }
}

Send the same request again and you get the same answer, with Idempotent-Replayed: true (replayed: true in the SDK). The player still has 100 gems. See Idempotency.

Step 4: Give the browser a token

The browser must act as one player. A player token (gc_pt_…) lets it do that for one hour, together with your publishable key. Only your server can ask for it, because only your server knows who is logged in.

LangageLanguage
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/tokens" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"
Answer
{ "token": "gc_pt_…", "expiresAt": "2026-10-05T22:41:51.000Z" }

Wrap it in an endpoint of your own, behind your login. This server uses only Node.js's built-in http module, so you can run it as it is. The line currentUser is the one to replace with your own session lookup.

server.mjs
import http from "node:http";
import { gamecoin, ext } from "./gamecoin.mjs";

// Replace with your own login: the id of the player who made this request.
const currentUser = (req) => req.headers["x-user-id"];

const server = http.createServer(async (req, res) => {
  const send = (status, body) => {
    res.writeHead(status, { "Content-Type": "application/json" });
    res.end(JSON.stringify(body));
  };

  const user = currentUser(req);
  if (!user) return send(401, { error: "not logged in" });

  try {
    // The browser asks for a token. It can only get one for the player it is logged in as.
    if (req.method === "POST" && req.url === "/api/gamecoin-token") {
      const { token } = await gamecoin.players.createToken(ext(user));
      return send(200, { token });
    }

    // The browser says a level is over. Your server decides if it is true, and what it is worth.
    if (req.method === "POST" && req.url === "/api/level-complete") {
      const level = 3;
      const { balance } = await gamecoin.wallet.grant(
        ext(user),
        { currency: "gems", amount: 10, reason: `level-${level}` },
        { idempotencyKey: `level-${level}-${user}` },
      );
      return send(200, { gems: balance.total });
    }

    send(404, { error: "not found" });
  } catch (error) {
    console.error("GameCoin:", error.code ?? error);
    send(502, { error: "GameCoin is not available" });
  }
});

server.listen(8080);

Never send the secret key, or a token for another player, to the browser.

Step 5: Start the browser SDK with the token

Pass the token as playerToken. The SDK then creates no player of its own: it plays as the player of the token, and the balance it reads is the one your server just set.

A token lasts an hour, so also pass onTokenExpired. The SDK calls it once, when a request is refused because the token expired, takes the token you return, and replays the request.

index.html
<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script type="module">
  const getToken = async () => {
    const response = await fetch("/api/gamecoin-token", { method: "POST" });
    return (await response.json()).token;
  };

  const gc = await GameCoin.init({
    publishableKey: "gc_pk_test_…",
    playerToken: await getToken(),
    onTokenExpired: getToken, // called once when the one-hour token is rejected
  });

  console.log(gc.balance("gems")); // 100: what your server granted
  gc.on("change", () => console.log("gems:", gc.balance("gems")));
</script>

From here the browser does the same things as in A game without a backend: gc.spend, gc.buyItem, gc.consume, gc.checkout. It can read, spend and pay. It still cannot credit anything by itself: the one exception is a gift code that you created, which the player can redeem with gc.redeemCode(code) for free coins and items, once. It works with your player token too; if you want your server to decide who may redeem, call the server route instead.

When your server grants something while the page is open (a reward after /api/level-complete), tell the SDK to read again:

JavaScript
await fetch("/api/level-complete", { method: "POST" });
await gc.refresh(); // the "change" event fires and your screen updates

Step 6: Let the server spend when the server must decide

The browser can spend by itself, which is simple and fine for a game where nothing depends on the purchase. When your server enforces the result (an item that unlocks something the server checks, an entry fee for a match your server runs), let the server spend, and the browser only asks:

LangageLanguage
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/purchases" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: potion-user-42-order-9" \
  -H "Content-Type: application/json" \
  -d '{"sku":"potion","quantity":2}'

purchases buys an item with its own price: the wallet is debited and the item added, in one step. If the wallet is too low, the call answers 409 INSUFFICIENT_FUNDS and nothing changes. The server SDK handles errors with GameCoinError; see Errors.

You can also read what your server needs:

LangageLanguage
curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"

curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/inventory" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"

Before you go live

  • The secret key lives only in your server's environment.
  • /api/gamecoin-token sits behind your login, and gives a token only for the logged-in player.
  • Every grant has an idempotency key made from the event.
  • Your browser code uses the publishable key and the token, nothing else.
  • You tried a declined payment, a player without enough gems, and an expired token. See Testing.

Where next