The gbandit runtime
The runtime is a small script the platform serves on your game's own address. Load it and you get, without any further code:
- The player:
gbandit.fetch()calls your backend as the player, andgbandit.loginUrl()andgbandit.signOut()send them to sign in and out (see Authentication). - Player counting: the heartbeat that counts your players for the games.gbandit.com ranking (see Player activity).
- Frontend logs: uncaught errors, unhandled rejections and
consoleoutput go togbandit logs frontend(see Frontend logs). - Test users locally: under
bun run devand other local dev servers you play as one of your project's test users, since gbandit sign-in only exists where the game is deployed. - Keyboard in the preview: on the platform's Build page your game runs in an iframe. Most game engines cancel
mousedown, which keeps a click from giving the iframe keyboard focus; the runtime gives it focus instead.
Games scaffolded from the gbandit template already load it. Every deploy whose index.html doesn't load it prints a warning.
Loading it
Put this in <head>, before your own scripts:
html
<script defer src="/__gbandit/runtime.js"></script>It's a same-origin path on your game's domain (/__gbandit/* is reserved for the platform). defer runs it after the page is parsed but before your own deferred and module scripts, so gbandit already exists when your code starts. It's a few kilobytes and is cached by the browser. A tag in <body>, or async, can run after your code instead, so keep it in <head>.
With Vite, add vite-ignore to the tag. Otherwise Vite warns that it can't bundle a classic script; it leaves the tag alone either way.
API
The runtime defines one global, gbandit. Types for TypeScript are in the template as frontend/src/gbandit-runtime.d.ts; copy that file into your project.
gbandit.fetch(path, init?)
js
const response = await gbandit.fetch("/api/me");
const me = await response.json();fetch to your own backend as the player. It adds their game token as Authorization: Bearer <token>, and if your backend answers 401 it fetches a fresh token and sends the request once more, which covers clock skew and a rotated signing key. A body has to survive being sent twice, so use a string, FormData or a Blob, not a stream.
It resolves only for a 2xx answer. Anything else, including no answer at all, rejects with a gbandit.GbanditError read from the Problem Details body your backend or gbandit answered with:
status: the HTTP status, or 0 when no response arrived.message: the body'sdetail, a sentence you can show the player.typeandis(slug): which problem it was.requestId: the id the request was logged under.
It only calls your game's own address and throws a TypeError for any other, since the token is the player's identity in your game.
The player may have no token: then fetch rejects before sending anything, with the same error getAccessToken() gives.
gbandit.getAccessToken(options?)
js
const token = await gbandit.getAccessToken();The player's game token, for what gbandit.fetch doesn't cover, such as a WebSocket (the bearer subprotocol). The runtime keeps it until shortly before it expires, and callers that ask at the same moment share one request, so ask for it each time instead of storing it. If your backend rejects a token the runtime still considers valid, ask again with { forceRefresh: true }.
A first-time visitor gets a guest automatically, so a token is almost always available. When there isn't one, the promise rejects with a GbanditError:
session-expiredorguest-limit-reached: send the player tologinUrl(). Aftersession-expiredthe runtime keeps refusing until the page loads again, because asking once more would replace the player's account with a new guest.account-suspended: the player can't play. Show themmessage.status0: no response at all, usually a network problem.
gbandit.loginUrl(redirect?)
js
location.href = gbandit.loginUrl();The sign-in page, which sends the player back to redirect (default: the current page) signed in. A guest who signs in keeps their progress; see Authentication. Navigate to it with a full page load: a router's client-side navigation can't reach it.
gbandit.profileUrl(redirect?)
Where the player changes their username, with a way back to redirect. See Letting players change their name.
gbandit.signOut(redirect?)
js
await gbandit.signOut();Signs the player out of gbandit, in every game, then navigates to redirect (default: your game's start page). The next visit gets a new guest.
gbandit.reportError(message, metadata?)
js
gbandit.reportError("level failed to load", { level: 3 });Sends an error to gbandit logs frontend without throwing one. metadata is any JSON.
Local development
On localhost or an IP address the runtime assumes a local dev server. It sends no heartbeats and reports no logs (DevTools has them), and it plays the auth service's part with your project's test users:
- A local run starts as
test-guest-1, the way a first visit in prod starts as a guest. ADEVbadge in the corner says who you are playing as. loginUrl(), and the badge, lead to a page where you pick anytest-user-<n>ortest-guest-<n>.signOut()goes back totest-guest-1.- Tokens come from your dev server, at
POST /__gbandit/token/user/<n>and/__gbandit/token/guest/<n>. Your local backend verifies them against the dev audience like any player's.
The template's frontend/vite.config.ts does the dev server's part under bun run dev. Any other dev server needs to answer four paths:
| Path | Answer |
|---|---|
GET /__gbandit/runtime.js | The output of gbandit runtime, which fetches the runtime from your game's dev address. |
GET /__gbandit/login | <script defer src="/__gbandit/runtime.js"></script>: the runtime draws the page itself. |
POST /__gbandit/token/user/<n> | The output of gbandit test-token <n> --json. |
POST /__gbandit/token/guest/<n> | The output of gbandit test-token <n> --guest --json. |
gbandit test-token needs gbandit login and a project that has been deployed once.