@clocktone/game-sdk
v1.0.0
Published
Cashable Unified Game SDK for embedded (WebView) and standalone (Capacitor) games
Readme
@clocktone/game-sdk
Unified TypeScript SDK for PlayLoop games. Provides a single API surface for both embedded (WebView inside the PlayLoop app) and standalone (Capacitor-wrapped APK) game modes.
Install
npm install @clocktone/game-sdkQuick Start
The npm package ships only the CDN loader — a ~1KB bootstrap that loads the full SDK from CDN at runtime. This keeps the SDK out of your bundle and enables over-the-air updates. See the Game Integration Guide for a complete walkthrough.
import { loadPlayLoopSDK } from '@clocktone/game-sdk/loader';
import type { PlayLoopSDKConfig } from '@clocktone/game-sdk/loader';
// Load SDK from CDN
const SDK = await loadPlayLoopSDK({ apiBaseUrl: 'https://api.vbackendapi.com' });
const sdk = new SDK({
gameId: 'smash-monsters',
debug: true,
});
await sdk.init();
// Show a rewarded video ad
const success = await sdk.ads.showRewardedVideo();
if (success) {
await sdk.rewards.awardCoins();
}
// Start play-time tracking
sdk.analytics.startAutoTracking();
// Clean up when done
sdk.dispose();Babylon.js Integration
The Babylon.js plugin is auto-detected at runtime and binds your scene for automatic freeze/resume:
// After SDK is loaded and initialized via the loader:
sdk.babylon.bindScene(scene, engine);
// The SDK will automatically stop/start the render loop
// when the host app freezes/resumes the gameModes
The SDK operates in two modes, auto-detected at runtime:
| | Embedded | Standalone |
|---|---|---|
| Context | WebView inside PlayLoop app | Capacitor-wrapped APK |
| Detection | window.ReactNativeWebView exists | window.Capacitor exists |
| Transport | postMessage bridge | HTTP REST calls |
| Auth | Host manages identity | App Set ID via Capacitor plugin |
| Ads | Host shows ads | Native AppLovin MAX plugin |
You can override detection by passing mode explicitly:
const sdk = new PlayLoopSDK({
gameId: 'my-game',
mode: 'standalone',
apiBaseUrl: 'https://api.vbackendapi.com',
appLovin: {
sdkKey: 'YOUR_SDK_KEY',
rewardedAdUnitId: 'YOUR_REWARDED_AD_UNIT_ID',
interstitialAdUnitId: 'YOUR_INTERSTITIAL_AD_UNIT_ID',
bannerAdUnitId: 'YOUR_BANNER_AD_UNIT_ID',
},
ui: {
onClose: () => Capacitor.Plugins.App.exitApp(),
},
});Configuration
interface PlayLoopSDKConfig {
/** Unique game identifier */
gameId: string;
/** Force a specific mode instead of auto-detecting */
mode?: 'embedded' | 'standalone';
/** Backend API base URL (standalone mode only) */
apiBaseUrl?: string;
/** Request timeout in milliseconds (default: 10000) */
requestTimeoutMs?: number;
/** Enable debug logging (default: false) */
debug?: boolean;
/** AppLovin MAX config (required for standalone mode ads) */
appLovin?: {
/** SDK key from AppLovin dashboard (Account > General > Keys) */
sdkKey: string;
rewardedAdUnitId: string;
interstitialAdUnitId: string;
/** Optional - omit to disable banner ads */
bannerAdUnitId?: string;
};
/** UI config for standalone mode */
ui?: {
onClose?: () => void; // Close button callback
onMenuOpen?: () => void; // Menu button callback (optional)
showPlayLoopButton?: boolean; // Show floating panel button (default: false)
playLoopButton?: {
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
};
};
}Note: The
appLovinanduiconfigs are only used in standalone mode. In embedded mode, the host PlayLoop app handles ads and UI.
Modules
The SDK exposes its functionality through focused modules:
| Module | Accessor | Description |
|---|---|---|
| Session | sdk.session | User identity, balance |
| Rewards | sdk.rewards | Coin awarding |
| Ads | sdk.ads | Rewarded video, interstitial & banner ads |
| Analytics | sdk.analytics | Play-time tracking, theme color |
| Babylon | sdk.babylon | Babylon.js render loop freeze/resume |
| Feature Flags | sdk.featureFlags | Remote config, feature gates, event logging (Statsig) |
| UI | sdk.ui | Floating panel, sponsored modal, offers, tier celebrations (standalone only) |
Authentication
Embedded mode: The host PlayLoop app handles all authentication. The SDK is always considered authenticated (sdk.isAuthenticated === true).
Standalone mode: The SDK reads the device's App Set ID via a Capacitor plugin and sends it as X-Install-Token header. The backend maps this to a known user if the user has previously opened the game in the PlayLoop app. If no mapping exists, sdk.isAuthenticated is false and coin-related features will fail gracefully.
if (sdk.isAuthenticated) {
const { coins } = await sdk.session.getBalance();
console.log(`User has ${coins} coins`);
} else {
console.log('User not linked - coin features unavailable');
}Lifecycle Events
// Called when the host app puts the game in the background
sdk.onFreeze(() => {
// Pause game logic, audio, etc.
});
// Called when the host app brings the game back
sdk.onResume(() => {
// Resume game logic, audio, etc.
});If you've bound a Babylon.js scene, the render loop is automatically stopped/started for you.
Error Handling
SDK errors include a code property for programmatic handling:
| Code | Meaning |
|---|---|
| TIMEOUT | Request timed out |
| TRANSPORT_ERROR | Communication layer failure |
| AD_NOT_AVAILABLE | No ad fill available |
| NETWORK_ERROR | HTTP request failed |
| USER_NOT_LINKED | Standalone user has no PlayLoop account mapping |
try {
await sdk.rewards.awardCoins();
} catch (err) {
if (err.code === 'USER_NOT_LINKED') {
// User hasn't opened this game in the PlayLoop app yet
}
}Cleanup
Always call dispose() when the SDK is no longer needed:
sdk.dispose();This stops analytics tracking, removes event listeners, aborts pending requests, and cleans up the Babylon.js plugin.
Top-Level Convenience APIs
PlayLoopSDK.getWidgetHeight(): number (static)
Computes the widget top-bar height from window.innerWidth alone — no DOM or init() required. Call it at script load time to offset your game canvas immediately:
const height = PlayLoopSDK.getWidgetHeight();
game.setTopOffset(height);Also available as a standalone function from both entry points:
// CDN / main entry
import { getWidgetHeight } from '@clocktone/game-sdk';
// Loader entry (works before CDN script loads)
import { getWidgetHeight } from '@clocktone/game-sdk/loader';sdk.onWidgetReady(handler): () => void
Fires after init() mounts the UI with the exact DOM-measured height. Use it to refine the initial estimate:
sdk.onWidgetReady(({ height }) => {
game.setTopOffset(height); // update with real value
});sdk.getTopBarHeight(): number
Returns the pixel height of the SDK's top bar. Before init() completes, falls back to PlayLoopSDK.getWidgetHeight() (computed estimate). After init(), returns the real DOM-measured value.
const offset = sdk.getTopBarHeight();
gameCanvas.style.marginTop = `${offset}px`;sdk.adConfig: Readonly<AdConfig>
Returns the current ad configuration with Statsig remote overrides merged on top of defaults. Shorthand for sdk.featureFlags?.adConfig ?? DEFAULT_AD_CONFIG.
const cooldown = sdk.adConfig.interstitialCooldownMs;See AdConfig for all available fields.
Standalone UI
In standalone mode, when the user is authenticated, the SDK renders a floating PlayLoopButton that opens a panel with rewards info, cashout flow, and games. It also provides sponsored video modals for interstitials and bonus/booster offer modals.
See the full UI Module documentation for all methods, components, and behavior details.
PlayLoopButton Panel
Shown automatically after init() when showPlayLoopButton: true. The panel has three content areas:
- Rewards tab (main screen) — coins pill, tier card with progress bar, "Convert coins to real money" CTA, partners section
- Cashout screen (sub-screen) — countdown timer, earnings display, cashout button, partners. Accessed via the CTA button; back button returns to main screen
- Games tab — lists available games
The panel trigger row displays the current coin balance with earn animations, inline countdown timer, and offer countdown badges.
Sponsored Interstitial
// Show a 3-second countdown modal, then the interstitial ad
const success = await sdk.ui?.showSponsoredInterstitial();Shows an animated countdown, then the ad, then a success/failure state. Coins are automatically awarded on success with the current boost multiplier applied.
Bonus & Booster Offers
The SDK periodically shows bonus coin offers and 2x boost offers via the OfferScheduler. When an offer triggers, a 20-second countdown badge appears on the panel trigger. If the user taps within the countdown, the corresponding modal opens (rewarded ad required to claim).
Controlling the UI
sdk.ui?.updateCoins(newBalance); // Manually update displayed coins
sdk.ui?.updateThemeColor('#1a1a2e'); // Change panel background
await sdk.ui?.refreshWallet(); // Re-fetch wallet data from backend
sdk.uiisnullin embedded mode or when the user is not authenticated.
Testing on Device
The SDK ships with two Android test targets for on-device testing. Both reference the SDK as a local file: dependency, so they always pick up the latest build.
| Target | Directory | App ID | Description |
|--------|-----------|--------|-------------|
| test-app | debug/ | com.playloop.monster | Minimal SDK debug harness |
| test-game | debug/test-game/block-blast/Block-Blast/ | com.playloop.monster | Block Blast game with full SDK integration |
Prerequisites
- Android device connected via USB with ADB debugging enabled
- Java 21 available (
JAVA_HOME=$(/usr/libexec/java_home -v 21))
Quick Start
# 1. Build the SDK
cd cashable-game-sdk && npm run build
# 2a. Deploy test-app (convenience script)
cd debug
JAVA_HOME=$(/usr/libexec/java_home -v 21) npm run run:android
# 2b. Deploy test-game (Block Blast)
cd debug/test-game/block-blast/Block-Blast
npm run build
npx cap sync android
cd android && JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew assembleDebug && cd ..
adb install -r android/app/build/outputs/apk/debug/app-debug.apkAPK Location
After building, the debug APK is at:
- test-app:
debug/android/app/build/outputs/apk/debug/app-debug.apk - test-game:
debug/test-game/block-blast/Block-Blast/android/app/build/outputs/apk/debug/app-debug.apk
Claude Code Skill
The sdk-android-test skill automates the full build-and-deploy flow. Invoke it with /sdk-android-test in Claude Code to build the SDK and deploy to a connected device.
Versioning
This package has two independent version tracks:
| Version | File | What it tracks | When to bump |
|---------|------|---------------|--------------|
| npm (loader + types) | package.json "version" | Loader code, type declarations, native plugins | Loader logic or exported types change |
| CDN (SDK runtime) | cdn-version.json "version" | Full SDK IIFE bundle served from CDN | SDK runtime code changes |
The npm package ships only the loader (~1KB bootstrap) and type declarations. The full SDK runtime is built separately and deployed to CDN (vbackendapi.com/sdk/v<CDN_VERSION>/playloop-sdk.js). At runtime, the loader fetches the SDK version from the backend and loads the IIFE bundle from CDN.
A CDN deploy does not require an npm publish, and vice versa.
Publishing
Automated (CI)
Push a tag matching sdk-v* to trigger the GitHub Actions workflow (.github/workflows/publish-sdk.yml):
# Bump version in package.json and/or cdn-version.json first
cd cashable-game-sdk
npm version patch # or minor/major — updates package.json version
# Tag and push
git add -A && git commit -m "chore(sdk): bump to v0.8.6"
git tag sdk-v0.8.6
git push origin main --tagsThe workflow will:
- Build the loader and CDN bundle
- Publish the npm package (
@clocktone/game-sdk) to the public registry - Upload the CDN bundle to R2 (
playloop-media/sdk/v<CDN_VERSION>/playloop-sdk.js)
Required GitHub secrets: NPM_TOKEN, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID
Manual
cd cashable-game-sdk
npm run build
npm publish --access public # publish loader to npm
CLOUDFLARE_ACCOUNT_ID=<id> wrangler r2 object put \
playloop-media/sdk/v0.7.0/playloop-sdk.js \
--file dist/cdn/playloop-sdk.js \
--content-type "application/javascript" \
--remote # upload CDN bundle to R2npm package
- Registry: https://www.npmjs.com/package/@clocktone/game-sdk
- Contents: Loader (~5KB), TypeScript types, Android Capacitor plugins, docs
- No secrets or business logic — all config is passed at runtime
R2 CDN bundle
- Bucket:
playloop-media(on the Mirko@playloop Cloudflare account) - Path:
sdk/v<VERSION>/playloop-sdk.js - Fallback URL:
https://getplayloop.com/sdk/v0.8.5/playloop-sdk.js
