What is Tula SDK?
The Tula SDK is a powerful JavaScript library that automatically optimizes your in-game store prices and quantities through intelligent A/B testing. It uses advanced bandit algorithms to continuously learn which price points and quantities drive the highest revenue and conversion rates for your game.
Real-time price optimization using Thompson Sampling bandit algorithms
Framework-agnostic auto-tracking with DOM data attributes - works with any JavaScript environment
Works with React, Unity, Cocos2d, vanilla JS, and any framework that renders DOM elements
Server-side verification and secure API key authentication
Install the Tula SDK using npm or yarn:
npm install tula-jsOr using yarn:
yarn add tula-jsInitialize the SDK with your API key and game configuration:
import TulaSDK from 'tula-js';
const tula = new TulaSDK({
apiKey: 'your_api_key_here',
debug: true, // Optional: enables debug logging
batchSize: 10, // Optional: event batch size (default: 10)
flushInterval: 5000, // Optional: auto-flush interval in ms (default: 5000)
platform: 'ios' // Optional: 'ios' | 'android' | 'both' (auto-detected)
});
// Initialize with player identification
await tula.initialize('player_123', {
level: 42,
vip: false,
country: 'US'
});
// Or let SDK auto-generate player ID
await tula.initialize(); // Auto-generates and persists player IDAutomatic Variant Caching & Event Tracking
Zero-Delay Pricing: During initialization, the SDK automatically fetches and caches all item variants for instantgetItemVariant() responses. No delays when showing pricing to users!
Auto-Tracking: The SDK automatically starts tracking events once initialized. Simply adddata-tula-store anddata-tula-item attributes to your DOM elements for automatic view tracking.
If no player ID is provided, the SDK will automatically generate one and persist it locally. This is useful for games that don't have user authentication.
The SDK maintains both an external player ID (your identifier) and an internal UUID for backend operations.
Add these attributes to your HTML for automatic event tracking:
<!-- Store page wrapper -->
<div data-tula-store data-tula-properties='{"section": "main"}'>
<h1>Game Store</h1>
<!-- Your store UI here -->
</div>
<!-- Individual item elements -->
<div data-tula-item data-tula-variant='{"itemId": "gold_coins", "variant": {...}}'>
<img src="coins.png" alt="Gold Coins" />
<p>$0.99 - 100 Coins</p>
<button>Buy Now</button>
</div>How Auto-Tracking Works
• Store views are tracked immediately when elements with data-tula-store are added to DOM
• Item views are tracked when elements with data-tula-item become 50% visible
• Works with any JavaScript framework: React, Unity, Cocos2d, vanilla JS, etc.
Important: Secure Your API Key
Never expose your API key in client-side code for web games. For mobile games, use environment variables or secure storage mechanisms.
Your API key authenticates your game with the Tula backend. You can find it in your dashboard under API Key Management.
The SDK automatically selects the optimal price and quantity combination for each item based on real-time performance data. This process happens transparently when you request an item variant.
// Get the optimized variant for a virtual item
const variant = await tula.getItemVariant('coins_pack');
// The variant contains all necessary information
console.log(variant.itemId); // Developer-friendly ID: "coins_pack"
console.log(variant.variant.productId); // App Store/Play Store product ID
console.log(variant.variant.price.formatted); // "$0.99"
console.log(variant.variant.quantity); // 100
console.log(variant.experimentId); // Current experiment ID if active
console.log(variant.experimentName); // Human-readable experiment name
console.log(variant.isExperiment); // true if part of A/B test
console.log(variant.isControl); // true if this is the control group
// Experiment view is automatically tracked when getting a variant
// if it's part of an active experimentTula automatically runs A/B tests on your virtual items, testing different price points and quantities to find the optimal combination. The SDK handles all the complexity:
Tula uses Thompson Sampling, a sophisticated bandit algorithm that balances exploration (trying new variants) with exploitation (using proven winners). This approach:
Creates a new instance of the SDK.
| Parameter | Type | Description |
|---|---|---|
| apiKey | string | Your Tula API key for authentication |
| debug? | boolean | Enable debug logging for development (default: false) |
| batchSize? | number | Number of events to batch before sending (default: 10) |
| flushInterval? | number | Auto-flush interval in milliseconds (default: 5000) |
| platform? | 'ios' | 'android' | 'both' | Target platform (auto-detected if not specified) |
const tula = new TulaSDK({
apiKey: 'your_api_key',
debug: true,
batchSize: 5,
flushInterval: 3000
});Initializes the SDK for a specific player.
| Parameter | Type | Description |
|---|---|---|
| playerId | string | Unique player identifier |
| metadata? | object | Optional player metadata |
Promise<void>
Resolves when initialization is complete
await tula.initialize('player_123', {
level: 42,
vip: false,
country: 'US'
});Returns the optimal variant for a virtual item instantly from cache. During initialization, all variants are pre-fetched and cached for zero-delay responses.
| Parameter | Type | Description |
|---|---|---|
| itemId | string | Developer-friendly item ID |
| platform? | 'ios' | 'android' | 'both' | Target platform (auto-detected by default) |
Promise<ItemVariantResponse>
The optimal variant with pricing and experiment context (returns instantly from cache)
{
virtualItemId: string, // Internal UUID - not exposed to developer
itemId: string, // Developer-friendly ID
experimentId: string | null,
experimentName?: string,
experimentArmId: string | null,
experimentArmName?: string,
variant: {
skuVariantId: string,
productId: string, // App/Play Store ID
price: {
cents: number,
dollars: number,
currency: string,
formatted: string // e.g., "$0.99"
},
quantity: number,
platform: 'ios' | 'android' | 'both',
productType: 'consumable' | 'non_consumable' | 'auto_renewable_subscription' | 'non_renewing_subscription'
},
platformSpecific: IOSPlatformData | AndroidPlatformData | null,
isExperiment: boolean,
isControl: boolean,
trafficWeight?: number,
metadata?: {
selectionReason: string,
algorithmUsed?: string,
userId?: string,
platform?: string,
timestamp: string
}
}// Returns instantly from cache (no network delay!)
const variant = await tula.getItemVariant('coins_pack');
console.log(variant.itemId); // "coins_pack"
console.log(variant.variant.price.formatted); // "$0.99"
console.log(variant.variant.quantity); // 100
console.log(variant.isExperiment); // true if A/B testing
// Works for any platform
const iosVariant = await tula.getItemVariant('coins_pack', 'ios');
const androidVariant = await tula.getItemVariant('coins_pack', 'android');Manually refreshes the variant cache by fetching all variants from the server. Useful if you want to update cached data during the session.
Promise<void>
Resolves when cache is refreshed with latest variants
// Refresh cache to get latest experiment assignments
await tula.refreshVariantCache();
// Now getItemVariant() will use updated data
const variant = await tula.getItemVariant('coins_pack');Returns the number of variants currently cached. Useful for debugging or displaying cache status.
number
Number of variants in the cache
const count = tula.getCachedVariantCount();
console.log(`${count} variants cached`); // "5 variants cached"Logs a custom event with experiment context.
| Parameter | Type | Description |
|---|---|---|
| eventType | 'store_view' | 'item_click' | 'purchase_start' | 'purchase_complete' | 'purchase_fail' | 'experiment_view' | Type of event to log |
| properties? | object | Additional event properties |
Promise<LogEventResponse>
Confirmation of logged event with metadata
{
success: boolean,
eventId: string,
playerId: string,
timestamp: string,
message: string,
metadata?: {
eventType: string,
experimentId?: string,
experimentArmId?: string,
processingTime: number
}
}await tula.logEvent('store_view', {
source: 'main_menu',
items_shown: 5
});Convenience method for tracking store views.
| Parameter | Type | Description |
|---|---|---|
| properties? | object | Additional event properties |
void
Synchronous method that queues the event
tula.trackStoreView({
source: 'level_complete'
});Convenience method for tracking item clicks with experiment context.
| Parameter | Type | Description |
|---|---|---|
| variant | ItemVariantResponse | The variant being clicked |
| properties? | object | Additional click properties |
Promise<LogEventResponse>
Confirmation of logged event
await tula.trackItemClick(variant, {
ui_element: 'store_button',
screen: 'main_store'
});Tracks when a user initiates a purchase.
| Parameter | Type | Description |
|---|---|---|
| variant | ItemVariantResponse | The variant being purchased |
| properties? | object | Additional purchase properties |
await tula.trackPurchaseStart(variant, {
source: 'store_page',
user_action: 'button_click'
});Tracks successful purchases with transaction details.
| Parameter | Type | Description |
|---|---|---|
| variant | ItemVariantResponse | The variant that was purchased |
| transactionId | string | Platform transaction ID |
| properties? | object | Additional purchase properties |
await tula.trackPurchaseComplete(variant, transaction.id, {
receipt: transaction.receipt,
verified: true
});Tracks failed or cancelled purchases.
| Parameter | Type | Description |
|---|---|---|
| variant | ItemVariantResponse | The variant that failed to purchase |
| error | string | Error message or reason |
| properties? | object | Additional error properties |
await tula.trackPurchaseFail(variant, error.message, {
error_code: 'USER_CANCELLED',
step: 'payment_authorization'
});Manually mark a DOM element for automatic store view tracking.
| Parameter | Type | Description |
|---|---|---|
| element | Element | DOM element to track |
| properties? | object | Additional event properties |
void
Element will be tracked when added to DOM
const storeElement = document.createElement('div');
tula.trackElementAsStore(storeElement, {
section: 'weapons'
});Manually mark a DOM element for automatic item view tracking.
| Parameter | Type | Description |
|---|---|---|
| element | Element | DOM element to track |
| itemVariant | ItemVariantResponse | Item variant data |
| properties? | object | Additional event properties |
void
Element will be tracked when 50% visible
const itemElement = document.createElement('div');
tula.trackElementAsItem(itemElement, variant, {
position: 1
});Enables automatic tracking of elements with data-tula-* attributes.
void
Auto-tracking starts immediately if SDK is initialized
tula.enableAutoTracking(); // Resume auto-trackingDisables automatic tracking of DOM elements.
void
Auto-tracking stops immediately
tula.disableAutoTracking(); // Switch to manual trackingTracks a custom event with a user-defined event type.
| Parameter | Type | Description |
|---|---|---|
| eventType | string | Custom event type name |
| properties? | object | Additional event properties |
void
Synchronous method that queues the event
tula.trackCustomEvent('level_complete', {
level: 5,
time_taken: 120,
score: 1500
});Returns the external player ID (developer-friendly ID).
string | null
The external player ID or null if not initialized
const playerId = tula.getPlayerId(); // "player_123"Returns the internal player UUID used by Tula backend.
string | null
The internal player UUID or null if not initialized
const internalId = tula.getInternalPlayerId(); // "abc-123-def-456"Checks if the SDK has been initialized.
boolean
True if initialized, false otherwise
if (tula.isInitialized()) {
// SDK is ready to use
}Manually flushes all queued events to the server.
Promise<void>
Resolves when all events are sent
await tula.flush(); // Force send all queued eventsCleans up the SDK, ends the session, flushes remaining events, and clears the variant cache.
Promise<void>
Resolves when cleanup is complete
await tula.destroy(); // Clean shutdown - clears cache and ends sessionThe easiest way to integrate Tula SDK is using automatic event tracking with data attributes:
import TulaSDK from 'tula-js';
class GameStore {
constructor() {
this.tula = new TulaSDK({
apiKey: process.env.TULA_API_KEY
});
}
async initialize(playerId) {
await this.tula.initialize(playerId, {
level: this.getPlayerLevel(),
vip: this.isVIPPlayer()
});
// Auto-tracking starts automatically after initialization
}
async loadStoreItems() {
// Fetch optimized variants for all items
const variants = [];
for (const item of this.items) {
const variant = await this.tula.getItemVariant(item.id);
variants.push({ ...item, variant });
}
// Render store with data attributes for auto-tracking
this.renderStoreWithAutoTracking(variants);
return variants;
}
renderStoreWithAutoTracking(variants) {
// Create store container with auto-tracking
const storeContainer = document.createElement('div');
storeContainer.setAttribute('data-tula-store', '');
storeContainer.setAttribute('data-tula-properties', JSON.stringify({
source: 'main_menu',
items_count: variants.length
}));
variants.forEach((item, index) => {
const itemElement = document.createElement('div');
// Auto-track item views when element becomes visible
itemElement.setAttribute('data-tula-item', '');
itemElement.setAttribute('data-tula-variant', JSON.stringify(item.variant));
itemElement.setAttribute('data-tula-properties', JSON.stringify({
position: index + 1,
category: item.category
}));
// Add your item UI
itemElement.innerHTML = `
<img src="${item.icon}" alt="${item.name}" />
<h3>${item.name}</h3>
<p>${item.variant.variant.price.formatted} - ${item.variant.variant.quantity} ${item.unit}</p>
<button onclick="purchaseItem('${item.id}')">Buy Now</button>
`;
storeContainer.appendChild(itemElement);
});
// Store view and item views are automatically tracked!
document.getElementById('store-root').appendChild(storeContainer);
}
async purchaseItem(itemId) {
const variant = this.getVariantById(itemId);
try {
// Track purchase start
await this.tula.trackPurchaseStart(variant);
// Process purchase...
const transaction = await this.processPlatformPurchase(variant.variant.productId);
// Track completion
await this.tula.trackPurchaseComplete(variant, transaction.id);
return { success: true, transaction };
} catch (error) {
// Track failure
await this.tula.trackPurchaseFail(variant, error.message);
return { success: false, error };
}
}
}Alternative: Manual Tracking Control
For games that need manual control over tracking, you can disable auto-tracking and use the utility methods:
tula.disableAutoTracking() andtula.trackElementAsStore(element)
For advanced use cases or canvas-only games, you can manually track events:
import TulaSDK from 'tula-js';
class GameStore {
constructor() {
this.tula = new TulaSDK({
apiKey: process.env.TULA_API_KEY
});
// Disable auto-tracking for manual control
this.tula.disableAutoTracking();
}
async initialize(playerId) {
await this.tula.initialize(playerId, {
level: this.getPlayerLevel(),
vip: this.isVIPPlayer(),
totalSpent: this.getPlayerLifetimeValue()
});
}
async loadStoreItems() {
// Manually track store view
await this.tula.logEvent('store_view', {
source: 'main_menu',
items_count: this.items.length
});
// Get optimized variants for each item
const variants = [];
for (const item of this.items) {
const variant = await this.tula.getItemVariant(item.id);
variants.push({ ...item, variant });
// Manually track item view
await this.tula.logEvent('experiment_view', {
itemId: variant.itemId,
skuVariantId: variant.variant.skuVariantId,
experimentId: variant.experimentId,
experimentArmId: variant.experimentArmId,
position: variants.length
});
}
// Track custom events for game-specific metrics
this.tula.trackCustomEvent('store_loaded', {
load_time: Date.now() - startTime,
items_count: variants.length,
player_level: this.getPlayerLevel()
});
return variants;
}
async purchaseItem(variant) {
try {
// Track purchase start
await this.tula.trackPurchaseStart(variant);
// Platform-specific purchase flow
const transaction = await this.processPlatformPurchase(
variant.variant.productId
);
// Track successful purchase
await this.tula.trackPurchaseComplete(
variant,
transaction.id,
{
receipt: transaction.receipt,
verified: await this.verifyReceipt(transaction)
}
);
// Grant items to player
this.grantItems(variant.variant.quantity);
return { success: true, transaction };
} catch (error) {
// Track failed purchase
await this.tula.trackPurchaseFail(
variant,
error.message,
{
error_code: error.code,
step: this.getPurchaseStep(error)
}
);
return { success: false, error };
}
}
async processPlatformPurchase(productId) {
// Platform-specific implementation
if (this.platform === 'ios') {
return await this.processStoreKitPurchase(productId);
} else if (this.platform === 'android') {
return await this.processPlayBillingPurchase(productId);
}
}
}Initialize the SDK as early as possible in your game's lifecycle, ideally right after player authentication. This ensures all events are properly tracked.
Use data-tula-store and data-tula-item attributes for automatic tracking. This reduces integration complexity and ensures consistent event tracking across your game.
The SDK works with React, Unity WebGL, Cocos2d, vanilla JS, and any framework that renders DOM elements. Choose the integration method that fits your tech stack.
Pass relevant player metadata during initialization (level, VIP status, spending history). This helps Tula segment players and optimize prices for different player types.
Always wrap SDK calls in try-catch blocks and have fallback pricing ready in case of network issues. Never let SDK errors break your purchase flow.
Fetch item variants during app startup to prevent UI flicker, but only track experiment views when items are actually displayed to users. Auto-tracking handles this timing automatically.
Always test your integration in sandbox environments before going live. Verify that purchases are tracked correctly and receipts are validated.
Regularly check your dashboard to monitor experiment performance. Look for significant changes in conversion rates and revenue to ensure everything is working correctly.
Always verify purchases server-side using receipt validation. Never trust client-side purchase data alone for granting items or currency.
API Key Invalid
Error: "Invalid API key" or 401 Unauthorized
Solution: Verify your API key in the dashboard. Make sure you're using the correct key for your environment (development vs. production).
Virtual Item Not Found
Error: "Virtual item 'item_name' not found"
Solution: Ensure the virtual item exists in your dashboard and is active. Check that you're using the correct item ID or name.
No SKU Variants Available
Error: "No SKU variants found for this item"
Solution: Create SKU variants for your virtual items in the dashboard. Make sure they're properly configured for your target platform.
SDK Not Initialized
Error: "SDK must be initialized before tracking events"
Solution: Call tula.initialize() before making any other SDK calls. Wait for the promise to resolve before proceeding.
Network Timeout
Error: Network request timeout or connection errors
Solution: Implement retry logic and fallback pricing. Cache variant data locally to handle temporary network issues.
Platform Detection Issues
Error: Incorrect platform detection or "both" platform returned
Solution: Explicitly specify the platform in SDK config: { platform: "ios" } or { platform: "android" }. Check console logs for platform detection details.
Event Batching Problems
Error: Events not being sent or delayed sending
Solution: Adjust batchSize and flushInterval settings, or call tula.flush() manually to force immediate sending.
Debug Mode
Enable debug mode to see detailed logs: new TulaSDK({ debug: true })
This will log all SDK operations, API calls, and responses to the console.
If you're still experiencing issues, please contact our support team: