PickBits SDK Reference
Drop-in JavaScript SDK for games and apps on *.pickbits.ai. Handles authentication, cross-subdomain single sign-on, saved progress, achievements, leaderboards, and premium entitlements. Version 1.x.
Install
Add a single <script> tag. The SDK is served from the main site, exposes window.PickBits, and self-loads the Supabase JS client from CDN.
<script src="https://pickbits.ai/sdk/pickbits-sdk.js"></script>
Initialise
Call PickBits.init() once with a gameSlug matching the slug registered in the achievements table.
PickBits.init({
gameSlug: 'remme',
economy: { // optional
type: 'coins',
name: 'Remme Credits',
icon: 'fa-coins',
getBalance: () => game.getCoins(),
storeUrl: '/store'
}
});
?pb_token= from the URL on init. If present, it exchanges the relay token for a Supabase session and strips the query param via history.replaceState. This is how cross-subdomain SSO lands the user automatically — see SSO.
Auth
| Method | Returns | Notes |
|---|---|---|
PickBits.isAuthenticated() | boolean | Sync. |
PickBits.getUser() | { id, email, username, display_name, avatar_url, level, subscription_tier, is_experimenter } | null | Sync. Populated after fetchProfile resolves. |
PickBits.onAuthChange(cb) | void | Fires on sign-in / sign-out. Passes the current user object or null. |
PickBits.promptLogin() | void | Redirects to pickbits.ai/login?redirect=<returnURL>. |
PickBits.promptSignup({ redirect }?) | void | Opens the Sign Up tab at /login?mode=signup. Returns to the current game URL by default, including after email confirmation. |
PickBits.isSubscriber() | boolean | Content entitlement; Arcade Pass is excluded. Existing content-tier behavior is unchanged. |
PickBits.isAdFree() | boolean | Arcade, Experimenter or Founder with a null/future subscription expiry, or a future ad_free_until donor/manual grant. Does not grant premium content. |
PickBits.startArcadePass(opts?) | Promise | Starts the $5/month Arcade Pass checkout and returns to this game on cancellation. Signed-out players go through signup; init() resumes checkout once after the relay exchange. Rejects if checkout fails. Main-site pages may pass { session: pbAuth.getSession() }. |
Load pickbits-ads.js after the SDK: it uses isAdFree() automatically in every integrated game. Keep University, bounties, Experimenter hub and roadmap gates on their existing content checks. Arcade Pass includes ad-free play and infinite levels; it does not grant content-tier access.
PickBits.startArcadePass().catch(function (error) {
showMessage(error.message);
});
Cross-subdomain SSO
Main-site → subdomain sign-in flow (Phase 1, pb_sso_phase1):
- User is signed in on
pickbits.ai. - Main site calls
window.pbAuth.linkTo('https://feedrunner.pickbits.ai/')— this mints an HMAC-signed relay token (60s TTL, single-use viaconsumed_relay_tokens) via therelay-tokenedge function and redirects with?pb_token=appended. - Destination subdomain loads the SDK, sees
?pb_token=, POSTsaction: "exchange"torelay-token, receives an OTP token hash, and callssupabase.auth.verifyOtp. A native Supabase session materialises in local storage — no additional credential entry.
window.pbAuth.signOut() on the main site calls the revoke-sessions edge function, which invokes supabase.auth.admin.signOut(jwt, "global"). Refresh tokens invalidate immediately; access tokens across subdomains expire within their 1h lifetime. No per-device session UI in v1.
Integrators on subdomains don't need to wire anything for SSO — just include the SDK and the token exchange is automatic. To initiate a cross-domain link from the main site, use pbAuth.linkTo().
Saved progress
| Method | Description |
|---|---|
PickBits.saveProgress(data, { slot }) | Saves to slot 0 by default. Payloads under 32 KB stay inline; larger payloads route through object storage when the v2 save flag is enabled. |
PickBits.loadProgress({ slot }) | Loads slot 0 by default. The legacy path returns the progress row; the v2 path returns the saved payload. |
PickBits.listSaveSlots() | Lists available save slots when v2 saves are enabled. |
// Autosave
setInterval(async () => {
if (PickBits.isAuthenticated()) {
await PickBits.saveProgress({ level: 7, inventory: game.getInventory() });
}
}, 30_000);
pb_sso_saves_v2. Calling saveProgress(data) or loadProgress() without an options object remains compatible and uses slot 0. When reading across both paths, unwrap with result.save_data ?? result.
Achievements
| Method | Description |
|---|---|
PickBits.unlockAchievement(slug) | Idempotent — calls claim-achievement edge function. Safe to replay. |
PickBits.getAchievements() | Returns this user's unlocks for this game + global achievements. |
PickBits.unlockAchievement('first-win').then(res => {
if (res.ok) toast(`+${res.xp_awarded} XP — ${res.achievement.title}`);
});
Leaderboards
PickBits.submitScore(score) posts to the submit-score edge function, which enforces "only update if higher" on the server. No client-side check required.
PickBits.submitScore(gameOverScore);
Named top scores
PickBits.getLeaderboard(gameSlug, { limit: 10 }) returns a promise for an array of { rank, display_name, username, score, updated_at }. It works without a session or init(). The limit defaults to 10 and is clamped to 1–100 by the server. Results use the all-time board, ordered by score descending, then oldest score timestamp first; ranks are numbered from 1. Missing display names fall back to username, then player.
PickBits.getLeaderboard('pinball-dreams', { limit: 5 }).then(function (rows) {
rows.forEach(function (row) {
console.log(row.rank, row.display_name, row.score);
});
}).catch(function (err) {
console.warn('Leaderboard unavailable:', err.message);
});
Your rank
PickBits.getMyRank(gameSlug) returns a promise for { rank, total }, using the signed-in player's saved all-time score. Rank is one plus the number of strictly higher scores, so equal scores share a rank. It resolves null when signed out, when the player has no score, or when a rank cannot be read. Call after authentication has finished; total counts players on that game's all-time board.
PickBits.getMyRank('pinball-dreams').then(function (standing) {
if (standing) console.log('#' + standing.rank + ' of ' + standing.total);
});
Infinite levels (v1)
PickBits.levels.list(gameSlug, { after: -1, limit: 50 }) returns a promise for live levels, ordered by level_index ascending. Each row contains { level_index, title, spec, difficulty, author_kind, credited_name }. Reads work without init(); RLS returns built-in seed levels to everyone and forged (LLM/human) specs only to ad-free members. An empty page resolves to []. The after index is exclusive and defaults to -1 (start at index 0); the page size defaults to 50. Pass the last received index to fetch the next page. The game owns the JSON spec format and its v version.
PickBits.levels.peek(gameSlug, { after: -1, limit: 50 }) reads arcade_levels_public, including all live forged levels for anonymous and free players. It uses the same exclusive index pagination as list() and returns { game_slug, level_index, title, author_kind, credited_name, difficulty, created_at }, with no spec. Use it for locked preview cards and credits.
PickBits.levels.canPlayInfinite() is the same entitlement as PickBits.isAdFree(): Arcade Pass ($5/month), Experimenter, Founder, or an active ad-free grant. Use startArcadePass() for Join; signed-out players are sent through promptSignup() with checkout intent preserved.
PickBits.levels.list('pickbits-putt', { after: 17, limit: 50 }).then(function (levels) {
levels.forEach(function (level) {
console.log(level.level_index, level.title, level.spec);
if (level.author_kind === 'llm' && level.credited_name) {
console.log('Edge forged by ' + level.credited_name);
}
});
}).catch(function (err) {
console.warn('Extra levels unavailable; keeping built-ins:', err.message);
});
PickBits.levels.reportFrontier(gameSlug, index) reports the player's highest completed level and resolves { max_live, queued }. It requires membership. A non-member RPC rejection (SQLSTATE 42501) becomes an Error with code: 'membership_required'; show the Arcade Pass upsell instead of logging a failure. It also requires a session and rejects with Error('Not authenticated') when signed out. The server keeps the player's highest index and ignores repeat reports within 10 seconds per player and game. When fewer than five live levels remain ahead of the reported index, it queues missing indices from max_live + 1 through index + 5. queued contains only indices newly queued by this call; repeats can return []. max_live is -1 if no live levels exist. Generation is asynchronous, so queued levels become readable only after publication.
// After completing the highest level currently available to the player:
PickBits.levels.reportFrontier('pickbits-putt', 22).then(function (result) {
console.log('Live through', result.max_live, 'new jobs', result.queued);
}).catch(function (err) {
if (err.code === 'membership_required') {
// Show "Members play beyond the built-ins"; Join calls PickBits.startArcadePass().
return;
}
console.warn('Frontier report skipped:', err.message);
});
pickbits-putt, pinball, and skeetime; the hub's own slug is infinite-arcade. These helpers take an explicit slug so the hub can read multiple games. Read and report failures reject their promises (rank lookup resolves null); keep built-in levels available for offline play.
Premium entitlements
Call on feature gates when an app needs to check the signed-in user's current platform entitlement.
| Method | Returns |
|---|---|
PickBits.isPremium() | Promise<boolean> — cached 60s in-memory. |
PickBits.getTier() | Promise<'free' | 'experimenter' | null> |
isPremium() is a UX optimisation — server-side edge functions re-check via the is_user_premium(uuid) SQL function for anything that costs real money (e.g. premium AI calls).
Analytics bridge
PickBits.trackEvent(name, props) forwards to PostHog if it's loaded on the page, enriching every event with pb_game, pb_authenticated, and pb_user_id.
PickBits.trackEvent('level_complete', { level: 7, duration_s: 142 });
Versioning
The SDK reads its version from PickBits.version (exposed in Phase 3) and sends it as a custom header on edge-function calls for server-side compatibility tracking. Breaking changes cut a major version; the <script> tag pins to the latest by default.
Support
Issues, questions, or integrations: open a ticket against PIC-17 on the internal board, or DM Dex on the agent network. SDK source lives at pickbits.ai/sdk/pickbits-sdk.js — no bundler, no npm publish.