Skip to main content

External games quick start

Build a game that runs inside the member portal in about 50 lines. Read the contract v1 for the full protocol. Here a small helper hides the boilerplate.

1. Add the helper​

Create incentia-sdk.js next to your index.html. It wraps the handshake and the two messages from the contract:

export function readPlayContext() {
const p = new URLSearchParams(location.search);
const sessionId = p.get('sessionId');
const playToken = p.get('playToken');
const hostOrigin = p.get('host');
return sessionId && playToken && hostOrigin ? { sessionId, playToken, hostOrigin } : null;
}

export function complete(ctx, signal, timeoutMs = 30000) {
return new Promise((resolve, reject) => {
const onMessage = (e) => {
if (e.origin !== ctx.hostOrigin) return;
if (e.data?.type !== 'incentia.outcome.v1' || e.data.sessionId !== ctx.sessionId) return;
cleanup();
resolve(e.data);
};
const timer = setTimeout(() => { cleanup(); reject(new Error('Outcome timeout')); }, timeoutMs);
const cleanup = () => { window.removeEventListener('message', onMessage); clearTimeout(timer); };
window.addEventListener('message', onMessage);
window.parent.postMessage(
{ type: 'incentia.complete.v1', sessionId: ctx.sessionId, signal }, ctx.hostOrigin);
});
}

The repository also contains a TypeScript version of this helper (tools/external-games-sdk, package name @incentia/games-sdk). It is not published on a public registry, so copy the helper above instead.

2. Wire the handshake​

readPlayContext returns null when the page is opened outside the host. Show a "dev mode" stub in that case.

<!doctype html>
<html>
<body>
<button id="play" disabled>Loading...</button>
<pre id="status"></pre>
<script type="module">
import { readPlayContext, complete } from './incentia-sdk.js';

const status = document.getElementById('status');
const play = document.getElementById('play');

const ctx = readPlayContext();
if (!ctx) {
status.textContent = 'No play context (open me through the host iframe)';
} else {
play.disabled = false;
play.textContent = 'Play';
play.addEventListener('click', async () => {
try {
const outcome = await complete(ctx, { score: 100 });
status.textContent = `Outcome: ${outcome.outcome}` +
(outcome.prize?.name ? ` — ${outcome.prize.name}` : '');
} catch (err) {
status.textContent = `Error: ${err.message}`;
}
});
}
</script>
</body>
</html>

When the player clicks, the helper posts the completion signal and waits for the host to return the outcome.

3. Zip and upload​

zip -r my-game.zip index.html incentia-sdk.js assets/

Upload the zip when you create or edit a game of type External in the admin. See Submission for the steps, allowed file types and size limits.

4. Develop locally​

Serve index.html from any static server. Without handshake parameters, readPlayContext returns null, so you can show a mock outcome and iterate without an IncentIA account.

if (!ctx) {
play.disabled = false;
play.textContent = 'Play (dev mock)';
play.addEventListener('click', () => {
status.textContent = 'Outcome: win — Dev prize';
});
}

5. Ship updates​

Upload a new zip to the same game. The game's bundle address is replaced in place, so there are no versions to manage and nothing to resubscribe. Plays already recorded do not keep a reference to the old bundle.

Next steps​