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 /socketgame: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