Skip to content

Keno Game Responses

This guide explains how to read the scenario field that placeBet returns for a keno game. It also explains how to use the loadConfig data to display the pay table.

Overview

Keno is a lottery-style game. The player picks 1–10 numbers from a grid of 40. The house draws random numbers. The engine pays the player based on how many picks match the draw. Each combination of pick count and risk level (low, medium, or high) has a different pay table. The grid size and draw count are configurable in the builder.

Keno is a single-result game. Each placeBet returns a complete result. There are no multi-step wager chains.

Sending Player Numbers

Every keno round requires a featureToBuy (pick count and risk). See Pick Count & Risk Selection via Buy Features for details. Send the player's chosen numbers via additionalData. If the request omits them, the engine auto-picks random numbers.

typescript
// Player selects their own numbers
await placeBet({
  backendURL,
  token,
  stake: 100,
  featureToBuy: 'picks_5_risk_medium',
  additionalData: { playerNumbers: [3, 7, 12, 25, 38] },
});

// Auto-pick - engine chooses the numbers
await placeBet({
  backendURL,
  token,
  stake: 100,
  featureToBuy: 'picks_5_risk_medium',
});

The number of playerNumbers must match the pick count for the selected feature. If the count does not match, the engine auto-picks instead.

The Keno Scenario

The engine enriches each placeBet response with the full draw result:

typescript
interface IKenoScenario {
  picks: number;           // How many numbers the player selected
  risk: string;            // Risk level: "low" | "medium" | "high"
  matches: number;         // How many player picks matched the draw
  multiplier: number;      // Payout multiplier for this outcome
  playerNumbers: number[]; // Player's chosen numbers (1-indexed, sorted)
  drawnNumbers: number[];  // House-drawn numbers (1-indexed, sorted)
  gridSize: number;        // Total numbers on the grid
  drawCount: number;       // How many numbers the house draws
}

Reading the Result

typescript
const response = await placeBet({
  backendURL, token, stake: 100,
  featureToBuy: 'picks_5_risk_medium',
  additionalData: { playerNumbers: [3, 7, 12, 25, 38] },
});

if (response.success) {
  const scenario = response.result.result.scenario as IKenoScenario;

  console.log('Your numbers:', scenario.playerNumbers);   // [3, 7, 12, 25, 38]
  console.log('House drew:', scenario.drawnNumbers);       // [2, 7, 15, 19, 25, 28, 31, 33, 37, 40]
  console.log(`${scenario.matches} matches, ${scenario.multiplier}x`);

  // Highlight matches
  const drawnSet = new Set(scenario.drawnNumbers);
  for (const num of scenario.playerNumbers) {
    if (drawnSet.has(num)) {
      console.log(`${num} - MATCH`);
    }
  }
}

Tile States for Rendering

Each number on the grid falls into one of four states:

Player picked?House drew?StateDisplay
YesYesMatchGreen highlight
YesNoMissBlue (player pick)
NoYesDrawnRed highlight
NoNoNeutralDefault

Pay Table from loadConfig

The loadConfig response includes a payTable object containing multiplier arrays for every pick count and risk level:

typescript
const config = configResponse.result.config;

const payTable = config.payTable as Record<string, Record<string, number[]>>;
// payTable["3"]["low"]    → [0, 1.5, 2, 4.5]
// payTable["7"]["medium"] → [0, 0, 0, 3, 6.5, 17, 75, 530]

config.gameType;    // 'keno'
config.gridSize;    // 40
config.drawCount;   // 10
config.minPicks;    // 1
config.maxPicks;    // 10
config.risks;       // ["low", "medium", "high"]

Each array has picks + 1 entries. Index 0 is the payout for 0 matches. Index 1 is the payout for 1 match. Later indexes follow the same pattern. An entry of 0 means no payout for that match count.

Pick Count & Risk Selection via Buy Features

Every combination of pick count and risk level is a buy feature. There is no default basegame. Every placeBet call must include a featureToBuy. The engine rejects a call without one with PARAMETERMISSING ("This game requires a buy feature. Provide featureToBuy.").

typescript
import { placeBet } from '@hizi.io/engine-sdk';

// Player selects 7 picks, medium risk, chooses their numbers
const featureId = `picks_${selectedPicks}_risk_${selectedRisk}`;

const response = await placeBet({
  backendURL,
  token,
  stake,
  featureToBuy: featureId,
  additionalData: { playerNumbers: [2, 8, 15, 22, 29, 33, 40] },
});

Single-Result Game

Keno is a single-result game. engineData.inProgress is false after every result. The game does not need continuation calls or collect calls.

typescript
const response = await placeBet({ backendURL, token, stake, featureToBuy: 'picks_2_risk_low', additionalData: { playerNumbers: [5, 10] } });
if (response.success) {
  const { scenario, engineData, totalWin } = response.result.result;
  // engineData.inProgress === false - round is complete
}

How the Math Works

Keno uses the hypergeometric distribution to compute match probabilities. This distribution gives the probability of a fixed number of successes when the engine samples without replacement. The player picks k numbers from a grid of N. The house draws D numbers. The formula gives the probability of exactly h matches:

P(h | k, N, D) = C(k, h) × C(N − k, D − h) / C(N, D)

C(n, r) is the binomial coefficient (read as "n choose r").

The risk level controls the payout shape:

  • Low risk: frequent small wins. Payouts start from fewer matches.
  • Medium risk: a balance between frequency and size. The threshold is moderate.
  • High risk: rare but large wins. Only the top match counts pay.

The engine auto-computes multipliers from the target RTP (return to player) and risk exponents. The builder has a "clean multipliers" option. When enabled, the engine snaps values to round numbers (for example, 1.5x, 5x, 75x). The engine also adjusts entry weights to maintain the target RTP exactly.

Provably Fair Verification

Keno is single-step. The PF (Provably Fair) chain commits to a single weighted entry draw. This draw fixes the match count. The engine seeds the cosmetic playerNumbers (when auto-picked) and drawnNumbers arrays from the PF commitment itself: (serverSeedHash, clientSeed). This lets the engine and pfVerify reproduce the same values without extra inputs.

Send the player picks if they chose their own

If the player typed their own playerNumbers, pass that array back to the verify call. If you omit it, the verifier auto-picks from the seed. Then the array does not match the live scenario.

Request

typescript
import { pfVerify } from '@hizi.io/engine-sdk';

const reply = await pfVerify({
  backendURL,
  token,
  serverSeed: pf.revealedServerSeed,
  clientSeed: pf.clientSeed,
  stake: 100,
  featureToBuy: 'picks_10_high',                          // the round's `picks_N_risk_X` package
  playerNumbers: [3, 11, 17, 22, 29, 31, 38, 40, 41, 44], // omit for auto-pick
});

Reading the response

Keno is one-shot. steps has one entry:

  • rngData: one int row (the entry weighted draw). It matches the live pf.rngData array exactly.
  • steps[0].scenario.matches: equals the live match count.
  • steps[0].scenario.playerNumbers / drawnNumbers: matches the live scenario element-wise.
  • steps[0].totalWin: equals the live multiplier.
typescript
if (!reply.success) return;
const { rngData, steps } = reply.result;
const matchOk = deepEqual(rngData, pf.rngData);
const drawOk = deepEqual(steps[0].scenario.drawnNumbers, liveScenario.drawnNumbers);
const winOk = steps[0].totalWin === liveResult.totalWin;

Next Steps