# 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.

## 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](/docs/guides/game-with-backend) instead.

## Before you start

You need a game and a catalog. The dashboard is in French for now.

1. [Create an account](/signup) and a game (« Nouveau jeu »). The two test keys are created with it.
2. Create the paid currency `gems` in « Monnaies » (type « Payante »).
3. Create the item `potion` in « Objets »: type « Consommable », « Prix en monnaie » 20 in `gems`, and tick « Achetable depuis le jeu (API cliente) ». Without that box the browser cannot buy it.
4. Create the pack `gems-500` in « Packs »: `gems` with « 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.
5. 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.

```html title="index.html"
<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.

```javascript
const catalog = await gc.getCatalog();
console.log(catalog.packs[0]);
```

```json title="One pack of the catalog"
{
  "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:

```javascript
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.

```javascript
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](/docs/sdk/browser#checkout).

## 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:

```javascript
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_…`.

```html title="index.html"
<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](/docs/concepts/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](/docs/guides/game-with-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](/docs/guides/game-with-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](/docs/sdk/browser): every option and call.
- [Selling packs](/docs/guides/selling-packs): VAT, return URLs and the life of an order.
- [Testing](/docs/guides/testing): the test environment, test players and the demo game.
