Appearance
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? | State | Display |
|---|---|---|---|
| Yes | Yes | Match | Green highlight |
| Yes | No | Miss | Blue (player pick) |
| No | Yes | Drawn | Red highlight |
| No | No | Neutral | Default |
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: oneintrow (the entry weighted draw). It matches the livepf.rngDataarray 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
- Response Handling: the
IGameResultstructure andengineDatafields. - Plinko Responses: another single-result game with risk levels.
- Buy Features: how buy features work.