v1.7.24

Studio API

External contract for integrating a game client (Unity / WebGL / Web) with the Aviator crash PoC over Phoenix Channels (WebSocket).

This document covers only the network protocol. The game server is deployed separately in the cloud — you connect to its public WebSocket URL.

PoC notes: no real payments or accounts. Each player gets a virtual balance of 1000 on first connect.


Endpoints

What Value
Socket path /socket
Channel game:lobby
URL example wss://<host>/socket (or ws:// in local/dev)

Install the Phoenix JS client (any environment that can speak Phoenix Channels):

npm install phoenix

Connecting

import { Socket } from "phoenix"
const playerId = "studio_player_001" // stable player id (string)
const socket = new Socket("wss://<host>/socket", {
params: { player_id: playerId }
})
socket.connect()
const channel = socket.channel("game:lobby", {})
channel
.join()
.receive("ok", (snapshot) => {
console.log("joined", snapshot)
// snapshot: phase, multiplier, balance, round_id, timings, ...
})
.receive("error", (err) => console.error("join failed", err))

Socket params

Param Type Description
player_id string Player id. If omitted, the server generates one. Persist it across sessions.

Join snapshot (ok)

{
"player_id": "studio_player_001",
"balance": 1000.0,
"phase": "betting",
"round_id": "round_a1b2c3d4e5f6",
"multiplier": 100,
"crash_point": null,
"phase_ends_at": 1710000005000,
"server_time": 1710000000000,
"betting_ms": 5000,
"crashed_ms": 3000,
"waiting_ms": 2000,
"tick_ms": 100,
"history": [],
"active_bets": 0
}
Field Description
player_id Your player id for this session (echo of the connect param).
balance Current wallet balance for this player.
phase Current round phase: betting, flying, crashed, or waiting.
round_id Id of the active round (changes each betting cycle).
multiplier Current multiplier in hundredths (100 = 1.00x).
crash_point Crash multiplier in hundredths; null until phases crashed / waiting.
phase_ends_at Unix ms when the current timed phase ends (for countdown UI).
server_time Server Unix ms at snapshot send time (for client clock skew).
betting_ms Configured duration of the betting phase in ms (UI hint only).
crashed_ms Configured duration of the crashed phase in ms (UI hint only).
waiting_ms Configured duration of the waiting phase in ms (UI hint only).
tick_ms Interval between multiplier tick events during flight (UI hint only).
history Recent finished rounds (newest first), each with round_id and crash_point.
active_bets Number of open bets in the current round.

phase_ends_at and server_time are Unix time in milliseconds.
Align timers with: skew = Date.now() - server_time.

Prefer phase + phase_ends_at over hardcoding durations. Snapshot fields *_ms are hints for UI only.


Multipliers = integer hundredths (×100)

All multiplier fields on the wire are integers:

Wire value Display
100 1.00x
251 2.51x
1000 10.00x
const displayX = multiplier / 100

crash_point is present only in phases crashed and waiting (hidden while the plane is flying).


Round phases

betting → flying → crashed → waiting → betting → …
Phase Typical duration What the client should do Allowed commands
betting ~5 s Show bet UI + countdown place_bet
flying until crash Animate multiplier / plane (~10 updates/s) cash_out
crashed ~3 s Play crash at crash_point —
waiting ~2 s Idle / prep next round —

During flight the server publishes ticks; approximate shape of growth (for smoothing between ticks if you want):

multiplier_hundredths ≈ round(exp(0.06 * t_seconds) * 100)

Do not use client-side multiplier for payouts. Authority is the server reply / cashout event.


Server → client events

channel.on("phase", (payload) => { /* phase change */ })
channel.on("tick", (payload) => { /* multiplier update */ })
channel.on("crash", (payload) => { /* plane crashed */ })
channel.on("bet_placed", (payload) => { /* someone bet (including you) */ })
channel.on("cashout", (payload) => { /* someone cashed out */ })
channel.on("welcome", (payload) => { /* optional housekeeping */ })

phase

{
"phase": "flying",
"round_id": "round_…",
"multiplier": 100,
"crash_point": null,
"phase_ends_at": null,
"server_time": 1710000000000,
"history": [
{ "round_id": "round_prev", "crash_point": 234 }
]
}

In crashed / waiting, crash_point is set (hundredths).

tick

~every 100 ms while phase === "flying".

{
"round_id": "round_…",
"multiplier": 127,
"server_time": 1710000000120
}
const x = payload.multiplier / 100

crash

{
"round_id": "round_…",
"crash_point": 351,
"phase": "crashed",
"phase_ends_at": 1710000003000,
"server_time": 1710000000000
}

After crash, any still-active bets are lost.

bet_placed

{
"player_id": "studio_player_001",
"slotId": 0,
"amount": 10.0,
"auto_cashout": 200,
"balance": 990.0,
"round_id": "round_…"
}

auto_cashout is hundredths (200 = 2.00x) or null.

cashout

{
"player_id": "studio_player_001",
"slotId": 0,
"amount": 10.0,
"multiplier": 185,
"payout": 18.5,
"balance": 1008.5,
"round_id": "round_…"
}

payout = amount * multiplier / 100 (rounded to 2 decimals).


Client → server commands

Each channel.push gets an ok / error reply. Related broadcasts may also go to all clients on the channel.

place_bet

Only in betting. One bet per player per round per slot (slotId).

channel
.push("place_bet", {
amount: 10,
slotId: 0, // optional (defaults to 0)
auto_cashout: 250 // optional hundredths (2.50x); float 2.5 also accepted
})
.receive("ok", (res) => console.log("bet ok", res))
.receive("error", (err) => console.warn("bet error", err.reason))
err.reason Meaning
not_betting_phase Not in betting phase
already_bet Already bet this round
amount_too_small < 1
amount_too_large > 10000
invalid_auto_cashout < 101 (1.01x)
insufficient_balance Not enough balance

cash_out

Only in flying, with an active bet. Optional slotId selects which slot to cash out (defaults to 0).

channel
.push("cash_out", { slotId: 0 }) // optional (defaults to 0)
.receive("ok", (res) => {
// res.payout, res.multiplier (hundredths), res.balance
})
.receive("error", (err) => console.warn(err.reason))
err.reason Meaning
not_flying_phase Not flying
no_bet No bet
already_cashed_out Already cashed out
bet_lost Round already lost

get_state

Fresh snapshot (e.g. after reconnect):

channel.push("get_state", {}).receive("ok", (state) => console.log(state))

Minimal client

import { Socket } from "phoenix"
function connectAviator({ url, playerId, onTick, onPhase, onCrash }) {
const socket = new Socket(url, { params: { player_id: playerId } })
socket.connect()
const channel = socket.channel("game:lobby", {})
channel.join().receive("ok", (snap) => onPhase?.(snap))
channel.on("tick", (p) => onTick?.(p.multiplier / 100, p))
channel.on("phase", (p) => onPhase?.(p))
channel.on("crash", (p) => onCrash?.(p.crash_point / 100, p))
return {
channel,
socket,
placeBet: (amount, autoCashoutHundredths, slotId = 0) =>
channel.push("place_bet", { amount, slotId, auto_cashout: autoCashoutHundredths }),
cashOut: (slotId = 0) => channel.push("cash_out", { slotId })
}
}
const game = connectAviator({
url: "wss://<host>/socket",
playerId: "artist_01",
onTick: (x) => { /* update plane / multiplier label */ },
onPhase: (p) => { /* switch scene by p.phase */ },
onCrash: (x) => { /* crash animation at x */ }
})
game.placeBet(25, 300, 0) // slot 0, auto cash out at 3.00x
game.placeBet(10, null, 1) // slot 1, no auto cash out
game.cashOut(0) // cash out slot 0

Integration checklist

  • [ ] Connect with Phoenix Channels to /socket → game:lobby
  • [ ] Persist player_id
  • [ ] On betting — bet UI + countdown from phase_ends_at
  • [ ] On tick — update multiplier (/ 100)
  • [ ] On crash — crash FX at crash_point / 100
  • [ ] Handle place_bet / cash_out errors via error.reason
  • [ ] Sync clocks with server_time
  • [ ] Never trust local multiplier for money — only server cashout / reply