Skip to content

Keno Game Responses

This guide explains how to read the scenario field returned by placeBet for a keno game, and 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, and the player is paid based on how many of their picks match the draw. Each combination of pick count and risk level (low/medium/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 + risk) — see Pick Count & Risk Selection via Buy Features. The player's chosen numbers are sent via additionalData; if omitted, 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 should match the pick count for the selected feature. If the count doesn't 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 for 1 match, etc. 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 must include a featureToBuy; a call without one will be rejected 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. No continuation calls or collect calls are needed.

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. The probability of exactly h matches when the player picks k numbers from a grid of N and the house draws D is:

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

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

The risk level controls the payout shape:

  • Low risk — more frequent small wins; payouts start from fewer matches
  • Medium risk — balanced; moderate threshold
  • High risk — rare but large wins; only the top match counts pay

Multipliers are auto-computed from the target RTP and risk exponents. When "clean multipliers" is enabled in the builder, values are snapped to round numbers (e.g. 1.5x, 5x, 75x) and entry weights are adjusted to maintain the target RTP exactly.

Provably Fair Verification

Keno is single-step: the PF chain commits to a single weighted entry draw that fixes the match count. The cosmetic playerNumbers (when auto-picked) and drawnNumbers arrays are seeded from the PF commitment itself — (serverSeedHash, clientSeed) — so the engine and pfVerify reproduce the same values without any 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 — otherwise the verifier will auto-pick from the seed and the array won't line up with 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). Array-equal against the live pf.rngData.
  • steps[0].scenario.matches — equals the live match count.
  • steps[0].scenario.playerNumbers / drawnNumbers — element-wise match against the live scenario.
  • 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