Integration guide

This page covers runtime modes, initialization config, accounts and email binding, cloud saves, leaderboards, analytics, ads, game-level storage and offline reconciliation. Method signatures live in the API reference.

How to include the SDK

ScenarioApproach
Hosted by the platform (instfun.games/play/{gameId}/)Load the CDN bundle <script src="/sdk/v1/instfun.js"></script> and use the global Instfun
Source integration built on the platformUse the same global object; import the SDK's TypeScript types when you need them

Accounts and wallets are shared across games only when they are served from the same origin; cross-origin iframe handshakes are not supported yet.

Runtime modes

modeUse caseWhere data lives
localStandalone play, backend-free developmentBrowser localStorage (falls back to memory when unavailable)
remoteProduction (requires the platform backend)Platform Worker + D1, server-authoritative
auto (default)Switch automatically by deploymentRemote when apiBaseUrl is present, local otherwise

apiBaseUrl resolution order (shared by auto and remote):

OrderConditionResult
1mode: "local"Local only; apiBaseUrl is ignored
2config.apiBaseUrl is non-emptyUse it
3window.INSTFUN_CONFIG.apiBaseUrl is non-empty (injected by the portal)Use it
4mode: "remote"Defaults to "/api/v1" (same-origin deployment)
5Anything else (auto with no URL)Local mode

Fallback rules for auto: the SDK falls back to local only when remote startup fails with the NETWORK error code. Configuration errors, UNAUTHORIZED and explicit remote are thrown as-is. After initialization sdk.mode is always "local" or "remote".

const sdk = await Instfun.init({
  gameId: "block-blast",
  mode: "remote",
  apiBaseUrl: "/api/v1" // default for same-origin deployment; may be omitted
});

sdk.mode; // resolved mode: "local" | "remote"

Capability differences between the two modes:

CapabilityLocal modeRemote mode
IdentitylocalStorage guest account (shared same-origin, locally generated 8-char account code)Server guest account + token (issued from deviceSeed, server account code)
Email bindingUnsupported; dialog shows an offline notice (unavailable)Email + verification code binding/rebinding
WalletLocal ledger (for standalone play)Server-authoritative ledger (idempotency, policies, daily caps)
Saves / recordsLocal storageLocal-first + server sync; offline goes to the outbox
LeaderboardOn-device best (localOnly: true)Global board (falls back to local when offline)
AnalyticsLocal queue; flush() only reports pendingBatched upload; clears on success
AdsBrowser simulation (simulated: true)Browser host still simulated (granted locally); other hosts use server-side receipt redemption
Game KVLocalLocal (server-side KV planned)

Initialization config

FieldTypeDescription
gameIdstring, requiredLetters, digits, underscores and hyphens only; must match game-info.json
gameVersionstringGame version; defaults to "0.0.0" and is reported with saves and events
debugbooleanEnable debug logging
mode"local" / "remote" / "auto"Defaults to auto
apiBaseUrlstringRemote API prefix; may be omitted for same-origin deployment
initialBalancePartial<Record<"coins" / "gems", number>>Initial balance in local mode
limits.saveThrottleMsnumberMinimum interval between direct save pushes; defaults to 2000ms
limits.sessionEventsnumberPer-process analytics event cap; defaults to 1000

Instfun is a singleton: calling init again replaces the current instance. The old object stays usable but is no longer returned by getInstance().

Accounts and identity

  • One guest account is shared by all games on the same origin. Local mode stores identity in instfun:v1:identity; remote mode exchanges a local deviceSeed for a server account and token, and re-logs into the same account when the token expires.
  • getCurrentUser() returns a UserProfile with userId, nickname, avatarUrl, bio, accountCode, email, accountBound, createdAt and recentGameIds.
  • accountCode is an 8-character code (digits plus upper/lowercase letters) for display and support; email is masked (for example a***@example.com) and is null when unbound.
  • updateProfile({ nickname?, avatarUrl?, bio? }) updates only those three fields.

Email binding

Guest accounts can bind an email for identification and cross-device recovery (remote mode only). Binding uses an SDK-rendered dialog (Shadow DOM isolated, bilingual, closable with ESC or a backdrop click).

const user = await sdk.account.getCurrentUser();
user.accountCode; // 8-character account code
user.email;       // masked email, or null when unbound
user.accountBound;

if (!(await sdk.account.hasEmail())) {
  const result = await sdk.account.openEmailBindDialog({
    // locale: "en", // defaults to the URL lang (?lang= on the site) then navigator.language
    onSuccess: (profile) => console.log("bound", profile.email),
    onCancel: () => console.log("cancelled")
  });
  // result: { status: "bound", email, profile } | { status: "cancelled" }
  //       | { status: "unavailable", reason: "offline" }
}

Flow and behavior:

  • Email input → send code (resend available after 60s by default; the server's resendAfterSeconds wins) → 6-digit code → verify. Errors are shown inline and a failed verification can be retried inside the dialog.
  • When the send-code response has alreadyBound: true (the email already belongs to the current account), the dialog skips code entry, refreshes the profile and resolves as bound.
  • Calling it again for a bound account shows the current masked email and a "Change email" entry that reuses the same flow.
  • In local mode (or without a DOM) the dialog shows "Email binding is unavailable offline" and returns unavailable(offline) without throwing; onCancel fires only when the user closes the dialog, presses ESC, or clicks the backdrop — never for unavailable.
  • A 401 during binding clears the token, re-logs as a guest with the local deviceSeed once and retries; if it still fails, the dialog shows an inline "session expired, refresh the page" message.
  • After a successful bind the SDK refreshes its cached UserProfile and hasEmail() returns true immediately.

Cloud saves and records

Both modes are local-first:

const progress = await sdk.game.loadProgress<{ level?: number }>();
await sdk.game.start({ level: progress?.data.level ?? 1 });
await sdk.game.saveProgress({ level: 4, bestScore: 8320 }, { merge: true });
const finish = await sdk.game.finish({
  result: "complete",
  score: 8320,
  submitScore: true
});
  • saveProgress(data, { merge }): merge is a shallow merge (top-level keys overwrite, nested values are replaced) and defaults to false. A serialized save over 256KB throws PAYLOAD_TOO_LARGE.
  • Saves carry a monotonically increasing revision. Remote pushes use baseRevision = revision - 1 for optimistic concurrency; on conflict the SDK fetches the server save, shallow-merges with the server as the base and retries once. A second conflict goes to the outbox until sync().
  • Remote mode merges pushes within a 2s window (limits.saveThrottleMs) into the outbox and returns pendingSync: true.
  • game.start(meta?) opens a session; a left-over unfinished session is recorded as quit. game.finish(input) settles the record and stats, writes input.progress when present, and submits to the leaderboard only when submitScore is true and score is a non-negative integer.
  • game.record(input) writes a record without a session; getStats() and getRecords({ limit? }) return stats and recent records (newest first, at most 50 kept).
  • On the first remote start, if the server has no save for the game, the existing local save is uploaded once (history migration).

Leaderboard

await sdk.leaderboard.submitScore({ score: 8320 });
const snapshot = await sdk.leaderboard.get({ boardId: "global", limit: 20 });
  • submitScore keeps only the better score; higherIsBetter defaults to true. When the direction differs from the stored record, comparability is considered void and the score overwrites it.
  • get returns a LeaderboardSnapshot: local mode gives scope: "local", localOnly: true and rank 1; remote mode gives scope: "global" and falls back to the local board when offline.
  • Your UI should branch on the snapshot: show "on-device best" when localOnly: true, render a global board when scope: "global".
  • The snapshot type includes window: "all" | "weekly". The weekly board exists on the server, but the SDK currently always requests window: "all" and does not expose the parameter.

Analytics

sdk.analytics.track("level_complete", { level: 4 });
const { sent, pending } = await sdk.analytics.flush();
  • track never throws: event names must be 1–64 characters; properties over 8KB serialized are dropped; the local queue keeps the newest 500 events and the per-process cap is 1000 (overridable with limits.sessionEvents).
  • Remote flush() uploads in batches of 100 and clears the queue only when every batch succeeds; failures keep the unsent tail. Local flush() performs no request and returns { sent: 0, pending }.
  • Inside play/{gameId}/ on the site, the SDK reads the page watermark <meta name="instfun-wm"> and reports it as properties.wm for attribution; an explicit wm passed by the game wins.

Ads

const ad = await sdk.ads.showRewarded({ placement: "DOUBLE_REWARD" });
// ad: { completed, simulated, rewardGranted, reward?, transactionId? }
const interstitial = await sdk.ads.showInterstitial({ placement: "GAME_OVER" });
// browser host returns { shown: false, simulated: true }
  • After a fully watched rewarded ad the SDK grants the reward idempotently (100 coins by default; override with currency, amount or reason). Do not add currency yourself.
  • The browser host simulates ads (simulated: true). When the window.InstfunWebHost bridge exists the ad runs in the host; in remote mode the server issues and redeems a reward receipt. The Android bridge is legacy compatibility.
  • supports("ads") === true only means the capability exists (possibly simulated). Do not use it to skip showRewarded; distinguish real from simulated with the simulated field.

Game-level storage

await sdk.storage.set("settings", { music: true });
const settings = await sdk.storage.get("settings");
const allKeys = await sdk.storage.keys();
sdk.storage.persistent; // false when localStorage is unavailable

Keys are namespaced per gameId, so identical keys in different games do not collide. When localStorage is unavailable the SDK degrades to memory, persistent is false, and you should warn players that progress will not be saved.

Lifecycle and offline reconciliation

await sdk.sync();
// { flushed, pending }: how many operations were sent and how many remain
  • In remote mode, failed save/record/leaderboard writes enter the outbox (up to 200 operations). sync() replays them in enqueue order; NETWORK, SERVER_ERROR and RATE_LIMITED are retryable, and the first retryable failure stops the replay while keeping the remaining operations. Non-retryable errors drop the operation and report an outbox_op_dropped event.
  • Call it after SDK initialization, when the page returns to the foreground (visibilitychange), when the network recovers (online), and at low-frequency points such as entering the lobby or the results screen. Do not call it every frame or after every round — sync() is subject to server rate limits.
  • In local mode sync() performs no network request and returns { flushed: 0, pending } from the local analytics queue; the shape matches remote mode, so game code needs no branching.

Security and quotas

Default thresholds (configurable on the server):

ItemDefault
Reads300/minute
Writes150/minute
Saves500/day (per user per game)
Records500/day (per user per game)
Leaderboard submissions300/day (per user per board)
Registrations50/IP/day
Request body64KB regular, 256KB for saves

SDK-side protection: remote save pushes are merged within a 2s window; saves over 256KB and event properties over 8KB are blocked or dropped locally. On RATE_LIMITED, saves, records and leaderboard submissions go to the outbox until the next sync(); wallet and ad rewards surface the error as-is and must not be retried automatically. Rate-limit and daily-quota 429s return Retry-After and InstfunError.details contains retryAfterSeconds, limit and window.

Integration notes

  • Branch leaderboard UI on localOnly / scope.
  • Ad rewards are written to the wallet by the SDK; never grant them twice.
  • In remote mode call sdk.sync() at startup, on foreground return and on network recovery.
  • When localStorage is unavailable, warn players that progress will not be saved.
  • Cross-game identity and wallet sharing require same-origin deployment; cross-origin iframes are not supported.
  • Wallet and ad rewards require connectivity in remote mode; everything else works offline and reconciles automatically.