Skip to content

Crash Game Responses

This guide explains how to drive a crash round end-to-end: opening the round with placeBet, listening to the websocket curve, and cashing out via collect.

Overview

Crash is a rules-based, real-time game. The engine commits a bust multiplier up front (kept secret server-side) and ticks a curve out over the websocket. The player's job is to cash out before the curve reaches the bust point.

The lifecycle uses three transports:

  1. HTTP/WS requestplaceBet opens the round and debits the stake. A placeBet made with a token that already references an open round resumes it instead (see Reconnecting Mid-Round).
  2. Server-pushed eventsroundStarted → repeated roundTickbetCashedOutroundEnded + balanceUpdated. On reconnect, the engine emits roundResumed in place of roundStarted.
  3. HTTP/WS requestcollect cashes out one or more bets at the live multiplier (only needed for manual cashout).

A round can carry multiple bets; each bet has its own hash and may use 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 that omits it must be cashed out via collect.

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 runs over the websocket from this point.

For a single manual-cashout bet you may 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 (via enableWebSockets) before calling placeBet — the curve is only delivered as server-pushed events. Since SDK 0.2.3 you don't have to wait for the handshake: a placeBet issued while the socket is still connecting is queued by the SDK and sent over the websocket once it opens (on older SDK versions such a call silently fell back to HTTP, leaving the round running with no curve events — always await enableWebSockets() there). 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 may fire one or more times per round depending on how many bets cashed out and when.

Manual Cashout

Take the bet hash from the roundStarted event's payload.betHashes (or the placeBet reply's scenario) and 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), so a late-arriving cashout against a curve that has already busted is rejected with GAMEROUNDNOTACTIVE.

You will see the result of a successful collect as a betCashedOut event (with isAuto: false).

Reconnecting Mid-Round

A round runs on the server independently of the client connection. If the player reloads the page or the websocket drops, the curve keeps ticking — the engine just stops sending broadcasts to a connection that isn't listening. To re-join, call placeBet again with the same token (the one carrying the open gameRound). A dropped SDK websocket reopens automatically on that call (SDK ≥ 0.2.3), so the resume both reconnects the socket and re-joins the round:

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

const response = await placeBet({ backendURL, token });

The request body's bets are ignored on resume — the round is already opened and debited. The engine instead returns the existing scenario in the reply and broadcasts a single roundResumed event with the live multiplier, the bet list (with cashout info for any bet that already settled), and the running totalWinValue. Normal roundTick / betCashedOut / 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 — same isolation as roundStarted and roundTick.

Failure modes the resume path surfaces back to placeBet:

StatusReturncodeMeaning
400GAMEROUNDALREADYSTARTEDThe round was already settled — clear the local round state and call placeBet again with bets. The reply may follow a roundEnded + balanceUpdated broadcast pair if the resume itself drove the stale round to settlement.
400GAMEROUNDNOTACTIVERound is no longer reachable (server-side crash secret missing). Treat as already-settled.
500DATASTRUCTUREWRONGOpen round exists but its game state is malformed — operator intervention required.

Scenario Shape

The placeBet response embeds an ICrashScenario on the round-opening game state entry:

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 committed bust multiplier is never returned to the client. The only way to learn where the curve crashed is to wait for the roundEnded event's payload.currentMultiplier.

Configuration

The creator-built loadConfig block is surfaced 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.postCashoutSpeedMultiplier

Frontends typically use startMultiplier and growth to render their own client-side preview curve between server ticks, and 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 just a viewport into it. placeBet opens a new round when called without an active gameRound on the token, and resumes the existing round when called with 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's seed-derived surface is the committed maxMultiplier — the curve's bust point. Everything else (the curve interpolation, the player's manual cashout time, auto-cashout settlement) is wall-clock or operator-audited, not derivable from the seeds. Use pfVerify to confirm the bust point was fixed before the round started.

Cashouts are not PF-derived

A passing pfVerify proves the engine couldn't have moved the bust point after committing to it. It does not verify your bet was settled at the right multiplier — that's a separate operator-audit concern (timestamps, multiplier-at-cashout reconciliation).

Request

No actions, no bets needed — the round's maxMultiplier is a function of the seeds alone.

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

One-shot — steps has one entry.

  • rngData — one double row (the entropy that derives the multiplier). Array-equal against the live pf.rngData.
  • steps[0].scenario.maxMultiplier — equals the multiplier the curve crashed at on the live ROUND_ENDED event.
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 line up the bust point was committed to before any bets were placed.

Next Steps

  • Response HandlingIGameResult structure and engineData fields.
  • Blackjack Responses — A rules-based game with a richer action tree.
  • Types ReferenceICrashScenario, ICrashBet, ICrashLoadConfig, ICrashRoundResumedPayload, TCrashEvent, CRASH_EVENTS, crashPlaceBetData, crashCollectData.