Appearance
Crash Game Responses
This guide explains how to drive a crash round from start to end. It covers three steps: opening a round with placeBet, listening to the curve on the websocket, and cashing out with collect.
Overview
Crash is a rules-based, real-time game. The engine commits a bust multiplier before the round starts. The engine keeps this multiplier secret on the server. The engine sends a curve of ticks over the websocket. The player must cash out before the curve reaches the bust point.
The lifecycle uses three transports:
- HTTP/WS request:
placeBetopens the round. It debits the stake. AplaceBetcall with a token that already references an open round resumes that round instead (see Reconnecting Mid-Round). - Server-pushed events:
roundStarted→ repeatedroundTick→betCashedOut→roundEnded+balanceUpdated. On reconnect, the engine sendsroundResumedinstead ofroundStarted. - HTTP/WS request:
collectcashes out one or more bets at the live multiplier (needed only for manual cashout).
A round can carry multiple bets. Each bet has its own hash. Each bet uses either auto-cashout or manual cashout.
Opening a Round
Send one bet per simultaneous wager. A bet that includes autoCashOutMultiplier settles automatically when the curve reaches that target. A bet without autoCashOutMultiplier needs a collect call to cash out.
typescript
import { placeBet, crashPlaceBetData } from '@hizi.io/engine-sdk';
const response = await placeBet({
backendURL,
token,
additionalData: crashPlaceBetData([
{ stake: 100, autoCashOutMultiplier: 2.5 }, // auto-cashout at 2.5x
{ stake: 100 }, // manual cashout
]),
});The total debit equals the sum of all stakes. The engine returns immediately. The round then runs over the websocket.
For a single manual-cashout bet, you can pass stake at the top level instead of using crashPlaceBetData:
typescript
await placeBet({ backendURL, token, stake: 100 });Listening to the Curve
You must enable the websocket connection with enableWebSockets before you call placeBet. The engine delivers the curve only as server-pushed events. Since SDK 0.2.3, you do not have to wait for the handshake. If you call placeBet while the socket is still connecting, the SDK queues the call. The SDK sends the call over the websocket once the socket opens. On older SDK versions, such a call fell back to HTTP silently. This left the round running with no curve events. On older SDK versions, always call await enableWebSockets() first. Each event arrives as a JSON message with an event name and a payload.
typescript
import { CRASH_EVENTS, type TCrashEvent } from '@hizi.io/engine-sdk';
socket.addEventListener('message', e => {
const data = JSON.parse(e.data);
if (!data.event) return; // request/response messages carry requestId, not event
const msg = data as TCrashEvent;
switch (msg.event) {
case CRASH_EVENTS.ROUND_STARTED:
// payload.betHashes - the engine-assigned hash for each bet, in submission order.
// payload.roundHash - round id (also returned as gameRound on the placeBet reply).
break;
case CRASH_EVENTS.ROUND_RESUMED:
// Sent in place of roundStarted when placeBet was a reconnect.
// payload.currentMultiplier - live curve position at the moment of resume.
// payload.betsSubmitted - full bet list with multiplierCollected / amountCollected
// populated for any bet that already settled while disconnected.
// payload.totalWinValue - cumulative winnings on this round so far.
// Render the curve at payload.currentMultiplier and reconcile bet state, then
// continue handling roundTick / betCashedOut / roundEnded as normal.
break;
case CRASH_EVENTS.ROUND_TICK:
renderCurve(msg.payload.currentMultiplier);
break;
case CRASH_EVENTS.BET_CASHED_OUT:
// One or more bets just settled (auto or manual).
// payload.isAuto distinguishes auto-cashout from /collect.
// payload.winValues[i] corresponds to payload.betHashes[i].
break;
case CRASH_EVENTS.ROUND_ENDED:
// Curve reached the committed bust point. Round is closing.
break;
case CRASH_EVENTS.BALANCE_UPDATED:
// Final per-bet credits and total winnings. Sent immediately after roundEnded.
break;
}
});roundTick ticks at loadConfig.timerIntervalMs (default 90 ms). betCashedOut can fire more than one time per round. The number of times depends on how many bets cash out and when they cash out.
Manual Cashout
Take the bet hash from the roundStarted event's payload.betHashes field, or from the placeBet reply's scenario. Call collect while the curve is still climbing.
typescript
import { collect, crashCollectData } from '@hizi.io/engine-sdk';
await collect({
backendURL,
token,
additionalData: crashCollectData(betHash),
});betHash accepts a single hash or an array. Pass an array to cash out multiple bets in one call. The engine pays each bet at min(liveMultiplier, maxMultiplier). If the cashout arrives after the curve busts, the engine rejects it with GAMEROUNDNOTACTIVE.
A successful collect call produces a betCashedOut event (with isAuto: false).
Reconnecting Mid-Round
A round runs on the server independent of the client connection. If the player reloads the page, or the websocket drops, the curve keeps ticking. The engine simply stops sending broadcasts to a connection that is not listening. To rejoin, call placeBet again with the same token (the token that carries the open gameRound). A dropped SDK websocket reopens automatically on that call (SDK 0.2.3 or later). So the resume call reconnects the socket and rejoins the round:
typescript
import { placeBet } from '@hizi.io/engine-sdk';
const response = await placeBet({ backendURL, token });The engine ignores the bets in the request body on resume. The round is already open and debited. Instead, the engine returns the existing scenario in the reply. The engine also sends a single roundResumed event. This event carries the live multiplier, the bet list (with cashout information for any bet that already settled), and the running totalWinValue. Normal roundTick, betCashedOut, and roundEnded events follow.
typescript
interface ICrashRoundResumedPayload {
/** Decimal live multiplier at the moment of resume. */
currentMultiplier: number;
/** ISO-8601 timestamp of when the curve started ticking. */
startTime: string;
/** Engine round id. */
roundHash: string;
/**
* Bets opened on the original placeBet, with collected /
* multiplierCollected / amountCollected populated for any that
* have already settled.
*/
betsSubmitted: ICrashBet[];
/** Current total winnings on this round (minor units). */
totalWinValue: number;
}The committed bust multiplier is intentionally absent. This matches the isolation used by roundStarted and roundTick.
The resume path can return these failure modes to placeBet:
| Status | Returncode | Meaning |
|---|---|---|
| 400 | GAMEROUNDALREADYSTARTED | The round is already settled. Clear the local round state. Call placeBet again with bets. The reply may include a roundEnded and balanceUpdated broadcast pair if the resume itself causes the stale round to settle. |
| 400 | GAMEROUNDNOTACTIVE | The round is no longer reachable (the server-side crash secret is missing). Treat the round as already settled. |
| 500 | DATASTRUCTUREWRONG | An open round exists. Its game state is malformed. This needs operator intervention. |
Scenario Shape
The placeBet response embeds an ICrashScenario object in the game-state entry that opens the round:
typescript
interface ICrashScenario {
/** ISO-8601 timestamp marking when the curve started ticking. */
startTime: string;
/** Bets posted on placeBet, in submission order. */
betsSubmitted: ICrashBet[];
/** Cashouts keyed by bet hash, populated as bets settle. */
betsCollected?: Record<string, { multiplier: number; amount: number }>;
}
interface ICrashBet {
/** Engine-assigned hash. Use this to cash out via crashCollectData. */
hash: string;
stake: number;
autoCashOutMultiplier?: number;
multiplierCollected?: number;
amountCollected?: number;
collected?: boolean;
inProgress?: boolean;
collectedAtinMS?: number;
}The engine never returns the committed bust multiplier to the client. To learn where the curve crashed, wait for the roundEnded event's payload.currentMultiplier.
Configuration
You can access the creator-built loadConfig block through loadConfig().result.config.loadConfig:
typescript
import type { ICrashLoadConfig } from '@hizi.io/engine-sdk';
const cfg = configResponse.result.config.loadConfig as ICrashLoadConfig;
// cfg.startMultiplier, cfg.growth, cfg.timerIntervalMs, cfg.startGameDelayMs,
// cfg.gameRtp, cfg.minMultiplier, cfg.maxMultiplier,
// cfg.postCashoutSpeedMultiplierFrontends typically use startMultiplier and growth to render a client-side preview curve between server ticks. Frontends use maxMultiplier to render the payout cap.
Game Flow
placeBet (with bets[])
└─ HTTP/WS reply: SUCCESS, round opened, balance debited
└─ event: roundStarted { betHashes, roundHash }
└─ event: roundTick { currentMultiplier } (every timerIntervalMs)
│
├─ auto-cashout target reached
│ └─ event: betCashedOut { isAuto: true, ... }
│
├─ client calls collect(crashCollectData(hash))
│ └─ event: betCashedOut { isAuto: false, ... }
│
├─ client reconnects with the same token → placeBet (no body bets)
│ └─ event: roundResumed { currentMultiplier, betsSubmitted, totalWinValue, ... }
│ └─ tick / cashout flow resumes
│
└─ curve reaches committed bust point
├─ event: roundEnded { currentMultiplier }
└─ event: balanceUpdated { totalWinValue, winValues }A crash round runs entirely on the server. The client connection is only a viewport into the round. placeBet opens a new round when the token has no active gameRound. placeBet resumes the existing round when the token has one. There is no other continuation flow.
Complete Example
typescript
import {
placeBet,
collect,
crashPlaceBetData,
crashCollectData,
CRASH_EVENTS,
type TCrashEvent,
} from '@hizi.io/engine-sdk';
async function playCrash(socket: WebSocket, target: number) {
let manualHash: string | null = null;
socket.addEventListener('message', e => {
const data = JSON.parse(e.data);
if (!data.event) return;
const msg = data as TCrashEvent;
if (msg.event === CRASH_EVENTS.ROUND_STARTED) {
// Two bets submitted: index 0 is auto, index 1 is manual.
manualHash = msg.payload.betHashes[1];
} else if (msg.event === CRASH_EVENTS.ROUND_TICK) {
renderCurve(msg.payload.currentMultiplier);
// Manual cashout the moment we cross our chosen target.
if (manualHash && msg.payload.currentMultiplier >= target) {
const hash = manualHash;
manualHash = null;
collect({ backendURL, token, additionalData: crashCollectData(hash) });
}
} else if (msg.event === CRASH_EVENTS.BET_CASHED_OUT) {
console.log(`cashed out at ${msg.payload.multiplier}x (auto=${msg.payload.isAuto})`);
} else if (msg.event === CRASH_EVENTS.ROUND_ENDED) {
console.log(`round ended at ${msg.payload.currentMultiplier}x`);
}
});
await placeBet({
backendURL,
token,
additionalData: crashPlaceBetData([
{ stake: 100, autoCashOutMultiplier: 2.0 },
{ stake: 100 },
]),
});
}Provably Fair Verification
Crash has one seed-derived surface: the committed maxMultiplier, which is the curve's bust point. (A seed-derived surface is the part of the outcome that the provably fair seeds determine.) Everything else is not derivable from the seeds. This includes the curve interpolation, the player's manual cashout time, and auto-cashout settlement. The engine tracks these by wall-clock time or by operator audit. Use pfVerify to confirm that the engine fixed the bust point before the round started.
Cashouts are not PF-derived
A passing pfVerify call proves the engine could not have moved the bust point after it committed to that point. It does not verify that the engine settled your bet at the right multiplier. That check is a separate operator-audit concern (timestamps, multiplier-at-cashout reconciliation).
Request
This request needs no actions and no bets. The seeds alone determine the round's maxMultiplier.
typescript
import { pfVerify } from '@hizi.io/engine-sdk';
const reply = await pfVerify({
backendURL,
token,
serverSeed: revealedServerSeed, // from the ROUND_ENDED broadcast
clientSeed: pf.clientSeed,
});Reading the response
This is a one-shot check: steps has one entry.
rngData: onedoublerow (the entropy that derives the multiplier). Compare it as an array against the livepf.rngData.steps[0].scenario.maxMultiplier: equals the multiplier where the curve crashed on the liveROUND_ENDEDevent.
typescript
if (!reply.success) return;
const { rngData, steps } = reply.result;
const rngOk = deepEqual(rngData, pf.rngData);
const bustOk = steps[0].scenario.maxMultiplier === liveCrashPoint;If both values match, the engine committed to the bust point before the player placed any bets.
Next Steps
- Response Handling: the
IGameResultstructure andengineDatafields. - Blackjack Responses: a rules-based game with a richer action tree.
- Types Reference:
ICrashScenario,ICrashBet,ICrashLoadConfig,ICrashRoundResumedPayload,TCrashEvent,CRASH_EVENTS,crashPlaceBetData,crashCollectData.