Skip to content

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

bash
npm install @kingstone/sdk

The SDK has zero runtime dependencies — it uses the built-in fetch API.

Step 2: Initialize the Client

Create a file called kingstone-test.ts:

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:

bash
export KINGSTONE_API_KEY=ks_sandbox_your_key_here
export KINGSTONE_BASE_URL=http://localhost:3001/api  # or your sandbox URL

Step 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:

ts
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 probability values must sum to exactly 1.0
  • sum(probability * payMultiplier) must equal certifiedRtp (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:

ts
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:

ts
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:

  1. KINGSTONE resolves your player ID (auto-creates on first spin)
  2. Deducts the wager from the player's credit balance
  3. Retrieves the next pre-computed outcome from the sealed queue
  4. Selects a visual presentation matching the outcome tier
  5. 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:

ts
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:

ts
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:

ts
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:

bash
KINGSTONE_API_KEY=ks_sandbox_xxx KINGSTONE_BASE_URL=http://localhost:3001/api npx tsx kingstone-test.ts

Troubleshooting

"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:

ts
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 equal certifiedRtp
  • A combination references a symbol name not in the symbols array
  • A combination references a tierId not in the tiers array

"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

KINGSTONE by Predigy Inc.