Quick Start
This guide takes you from "I have an API key" to "I executed my first spin" in under 10 minutes.
Prerequisites
- Node.js 20+ installed (download)
- A KINGSTONE sandbox API key (provided by Predigy — starts with
ks_) - The sandbox base URL (provided by Predigy, e.g.,
https://sandbox.kingstone.dev/api)
Step 1: Install the SDK
npm install @kingstone/sdkThe SDK has zero runtime dependencies — it uses the built-in fetch API.
Step 2: Initialize the Client
Create a file called kingstone-test.ts:
import { KingstoneClient } from '@kingstone/sdk';
const client = new KingstoneClient({
apiKey: process.env.KINGSTONE_API_KEY!,
baseUrl: process.env.KINGSTONE_BASE_URL,
sandbox: true,
});Set your environment variables:
export KINGSTONE_API_KEY=ks_sandbox_your_key_here
export KINGSTONE_BASE_URL=http://localhost:3001/api # or your sandbox URLStep 3: Upload a PAR Sheet
A PAR sheet (Probability Accounting Report) defines the math model for a game — symbols, prize tiers, probabilities, and payouts. KINGSTONE uses this to pre-compute all outcomes.
Here is a minimal 3-reel PAR sheet you can use for testing:
const parSheet = {
gameTitle: 'Sandbox Classic 3-Reel',
manufacturer: 'Partner Test Studio',
numReels: 3,
virtualStopsPerReel: [32, 32, 32],
certifiedRtp: 0.925,
denominationUsd: 0.25,
symbols: [
{ name: 'Seven', display: '7' },
{ name: 'Bar', display: 'BAR' },
{ name: 'Cherry', display: 'CH' },
{ name: 'Plum', display: 'PL' },
{ name: 'Blank', display: 'BL' },
],
tiers: [
{ tierId: 0, name: 'No Win', probability: 0.7, payMultiplier: 0 },
{ tierId: 1, name: 'Small Win', probability: 0.15, payMultiplier: 2 },
{ tierId: 2, name: 'Medium Win', probability: 0.08, payMultiplier: 3 },
{ tierId: 3, name: 'Large Win', probability: 0.04, payMultiplier: 4 },
{ tierId: 4, name: 'Big Win', probability: 0.02, payMultiplier: 5 },
{ tierId: 5, name: 'Huge Win', probability: 0.008, payMultiplier: 6 },
{ tierId: 6, name: 'Jackpot', probability: 0.002, payMultiplier: 38.5 },
],
combinations: [
{ reelSymbols: ['Blank', 'Blank', 'Blank'], comboString: 'BL|BL|BL', payMultiplier: 0, comboProbability: 0.7, tierId: 0, displayWeight: 1 },
{ reelSymbols: ['Cherry', 'Cherry', 'Blank'], comboString: 'CH|CH|BL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Plum', 'Plum', 'Plum'], comboString: 'PL|PL|PL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Cherry', 'Cherry', 'Cherry'], comboString: 'CH|CH|CH', payMultiplier: 3, comboProbability: 0.08, tierId: 2, displayWeight: 1 },
{ reelSymbols: ['Bar', 'Bar', 'Bar'], comboString: 'BAR|BAR|BAR', payMultiplier: 4, comboProbability: 0.04, tierId: 3, displayWeight: 1 },
{ reelSymbols: ['Bar', 'Seven', 'Bar'], comboString: 'BAR|7|BAR', payMultiplier: 5, comboProbability: 0.02, tierId: 4, displayWeight: 1 },
{ reelSymbols: ['Seven', 'Seven', 'Bar'], comboString: '7|7|BAR', payMultiplier: 6, comboProbability: 0.008, tierId: 5, displayWeight: 1 },
{ reelSymbols: ['Seven', 'Seven', 'Seven'], comboString: '7|7|7', payMultiplier: 38.5, comboProbability: 0.002, tierId: 6, displayWeight: 1 },
],
};
const uploaded = await client.uploadParSheet(parSheet);
console.log(`PAR sheet created: ID ${uploaded.parSheetId}`);Math Requirements
- All
probabilityvalues must sum to exactly 1.0 sum(probability * payMultiplier)must equalcertifiedRtp(e.g., 0.925)- KINGSTONE validates the math on upload and rejects invalid data
Step 4: Register a Game
Link the PAR sheet to a game configuration with wager limits:
const game = await client.registerGame({
parSheetId: uploaded.parSheetId,
displayName: 'My Test Slot',
configName: 'TEST_SLOT_001',
minWagerUsd: 0.10,
maxWagerUsd: 5.00,
wagerIncrementUsd: 0.10,
});
console.log(`Game registered: ID ${game.gameConfigId}`);Step 5: Execute a Spin
This is the core operation. Send a spin request with the game ID, your player's ID, and the wager amount:
const result = await client.spin({
gameId: game.gameConfigId,
playerId: 'player-001', // Your external player ID
wagerUsd: 0.25, // Must be within min/max/increment
});
console.log('--- Spin Result ---');
console.log(`Spin ID: ${result.spinId}`);
console.log(`Prize Tier: ${result.outcome.prizeTier}`);
console.log(`Payout: $${result.outcome.payoutUsd.toFixed(2)}`);
console.log(`Combo: ${result.presentation.comboString}`);
console.log(`Balance: $${result.balance.balanceUsd.toFixed(2)}`);What happens behind the scenes:
- KINGSTONE resolves your player ID (auto-creates on first spin)
- Deducts the wager from the player's credit balance
- Retrieves the next pre-computed outcome from the sealed queue
- Selects a visual presentation matching the outcome tier
- Credits winnings (if any) and returns the full result
The player's balance starts with sandbox test credits. In production, you manage deposits and withdrawals through your own payment system.
Step 6: Check a Settlement Report
Settlement reports are generated daily at 02:00 UTC. After running some spins, you can fetch them:
const { reports } = await client.listSettlements({ page: 1, limit: 5 });
for (const report of reports) {
console.log(`${report.reportDate}: ${report.totalSpins} spins, GGR $${report.ggrUsd.toFixed(2)}`);
}Step 7: Set Up a Webhook (Optional)
Receive real-time notifications for large wins, settlement reports, and more:
const webhook = await client.registerWebhook({
url: 'https://your-server.com/webhooks/kingstone',
events: ['spin.large_win', 'settlement.report_ready'],
});
// IMPORTANT: Save the secret — it is shown only once
console.log(`Webhook ID: ${webhook.id}`);
console.log(`Secret: ${webhook.secret}`);See the Webhooks Guide for HMAC signature verification.
Complete Working Example
Here is the full script combining all the steps above:
import { KingstoneClient, KingstoneApiError } from '@kingstone/sdk';
async function main() {
// Initialize
const client = new KingstoneClient({
apiKey: process.env.KINGSTONE_API_KEY!,
baseUrl: process.env.KINGSTONE_BASE_URL,
sandbox: true,
});
try {
// Check if we already have games registered
const { games } = await client.listGames();
let gameId: number;
if (games.length > 0) {
gameId = games[0].gameConfigId;
console.log(`Using existing game: ${games[0].displayName} (ID: ${gameId})`);
} else {
// Upload PAR sheet + register game (see Steps 3-4 above)
const uploaded = await client.uploadParSheet({
gameTitle: 'Sandbox Classic 3-Reel',
manufacturer: 'Partner Test Studio',
numReels: 3,
virtualStopsPerReel: [32, 32, 32],
certifiedRtp: 0.925,
denominationUsd: 0.25,
symbols: [
{ name: 'Seven', display: '7' },
{ name: 'Bar', display: 'BAR' },
{ name: 'Cherry', display: 'CH' },
{ name: 'Plum', display: 'PL' },
{ name: 'Blank', display: 'BL' },
],
tiers: [
{ tierId: 0, name: 'No Win', probability: 0.7, payMultiplier: 0 },
{ tierId: 1, name: 'Small Win', probability: 0.15, payMultiplier: 2 },
{ tierId: 2, name: 'Medium Win', probability: 0.08, payMultiplier: 3 },
{ tierId: 3, name: 'Large Win', probability: 0.04, payMultiplier: 4 },
{ tierId: 4, name: 'Big Win', probability: 0.02, payMultiplier: 5 },
{ tierId: 5, name: 'Huge Win', probability: 0.008, payMultiplier: 6 },
{ tierId: 6, name: 'Jackpot', probability: 0.002, payMultiplier: 38.5 },
],
combinations: [
{ reelSymbols: ['Blank','Blank','Blank'], comboString: 'BL|BL|BL', payMultiplier: 0, comboProbability: 0.7, tierId: 0, displayWeight: 1 },
{ reelSymbols: ['Cherry','Cherry','Blank'], comboString: 'CH|CH|BL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Plum','Plum','Plum'], comboString: 'PL|PL|PL', payMultiplier: 2, comboProbability: 0.075, tierId: 1, displayWeight: 1 },
{ reelSymbols: ['Cherry','Cherry','Cherry'], comboString: 'CH|CH|CH', payMultiplier: 3, comboProbability: 0.08, tierId: 2, displayWeight: 1 },
{ reelSymbols: ['Bar','Bar','Bar'], comboString: 'BAR|BAR|BAR', payMultiplier: 4, comboProbability: 0.04, tierId: 3, displayWeight: 1 },
{ reelSymbols: ['Bar','Seven','Bar'], comboString: 'BAR|7|BAR', payMultiplier: 5, comboProbability: 0.02, tierId: 4, displayWeight: 1 },
{ reelSymbols: ['Seven','Seven','Bar'], comboString: '7|7|BAR', payMultiplier: 6, comboProbability: 0.008, tierId: 5, displayWeight: 1 },
{ reelSymbols: ['Seven','Seven','Seven'], comboString: '7|7|7', payMultiplier: 38.5, comboProbability: 0.002, tierId: 6, displayWeight: 1 },
],
});
const game = await client.registerGame({
parSheetId: uploaded.parSheetId,
displayName: 'My Test Slot',
configName: 'TEST_SLOT_001',
minWagerUsd: 0.10,
maxWagerUsd: 5.00,
wagerIncrementUsd: 0.10,
});
gameId = game.gameConfigId;
console.log(`Created game: ${game.displayName} (ID: ${gameId})`);
}
// Execute 5 spins
for (let i = 1; i <= 5; i++) {
const result = await client.spin({
gameId,
playerId: 'quickstart-player',
wagerUsd: 0.25,
});
const won = result.outcome.payoutUsd > 0 ? `WIN $${result.outcome.payoutUsd.toFixed(2)}` : 'No win';
console.log(`Spin ${i}: ${result.presentation.comboString} — ${won} (Balance: $${result.balance.balanceUsd.toFixed(2)})`);
}
} catch (error) {
if (error instanceof KingstoneApiError) {
console.error(`API Error [${error.errorCode}]: ${error.message} (HTTP ${error.statusCode})`);
} else {
throw error;
}
}
}
main();Run it:
KINGSTONE_API_KEY=ks_sandbox_xxx KINGSTONE_BASE_URL=http://localhost:3001/api npx tsx kingstone-test.tsTroubleshooting
"Missing X-API-Key header" (KS-4001)
You forgot to include the API key. Make sure KINGSTONE_API_KEY is set in your environment, or pass it directly to the constructor.
"Invalid API key" (KS-4002)
The key does not match any record. Double-check:
- The key was copied correctly (no trailing spaces)
- You are using a sandbox key (
ks_) against the sandbox URL - The key has not been revoked
"Wager amount out of range" (KS-4103)
Your wager is below the minimum, above the maximum, or not on the increment boundary. Check the game's wager limits:
const { games } = await client.listGames();
console.log(games[0].minWagerUsd, games[0].maxWagerUsd, games[0].wagerIncrementUsd);"Invalid PAR sheet data" (KS-4104)
The PAR sheet math does not validate. Common causes:
- Probabilities do not sum to exactly 1.0 (check for floating-point rounding)
sum(probability * payMultiplier)does not equalcertifiedRtp- A combination references a symbol name not in the
symbolsarray - A combination references a
tierIdnot in thetiersarray
"Rate limit exceeded" (KS-4291 / KS-4292)
You are sending too many requests. Check the X-RateLimit-Remaining and Retry-After response headers. Default limits:
- Spins: 300/minute
- Other API calls: 600/minute
Wait for the Retry-After period, then retry. See Rate Limits for details.
"Game outcome queue exhausted" (KS-4203)
All pre-computed outcomes have been consumed and new blocks have not been generated yet. This is rare — KINGSTONE auto-generates new blocks when reserves drop below 2. In sandbox, blocks are only 100 entries, so this can happen with rapid testing. Wait a minute and retry.
Next Steps
- Executing Spins — detailed spin response format and frontend integration
- Settlements — daily reconciliation workflow
- Webhooks — real-time event notifications
- Error Handling — retry logic and error code reference
