How it works

One Bun process. One port. Four parts. Each part is one interface in @open-rgs/contract — swap any of them without touching the others.

The picture

flowchart TD
  Client([CLIENT])
  subgraph RGS["one Bun process · one port"]
    direction TB
    Transport["TRANSPORT"]
    Orch["ORCHESTRATOR<br/>· Lua math via wasmoon"]
    Transport --> Orch
  end
  Admin["admin http<br/>/livez · /healthz<br/>/admin/manifest<br/>/admin/autoclose"]
  Adapter["PLATFORM ADAPTER"]
  Operator([OPERATOR])

  Client -->|"binary-msgpack ws<br/>frame = type + msgpack"| Transport
  Admin -. http .-> Orch
  Orch -->|"PlatformAdapter<br/>openSession · settleSimple<br/>openComplex · closeComplex"| Adapter
  Adapter -->|"operator wire — ws · rest · grpc · …<br/>one WS, or three services"| Operator

The four parts

PartOwnsYou usually
Transport WS upgrade, frame decode, admin HTTP on the same port use the bundled binaryTransport
Orchestrator round lifecycle, mode resolution, bet computation, promo free-rounds, max-win cap, autoclose, metrics never touch — that's the framework
Math RTP, ops, awaiting hints. Currency-blind. RNG injected via host write one Lua file per game shape
Adapter session lifecycle, money movement, promo free-rounds, event stream pick one (platform-mock dev, adapter-your-provider prod, or write your own)

A round, end to end

PhaseWire frameWallet RPCMath call
connect
initINITopenSession
simple spinSPINsettleSimpleplay
complex openOPENopenComplexopen
complex step (× N)STEPstep
complex closeCLOSEcloseComplexclose
autoclose (external)closeComplexautoclose | policy
resumeINIT (with resume payload)openSession

Money moves once per simple round and twice per complex round — never more. STEP is pure in-process: the math computes the next visual + state, no wallet is touched. Cash-out on a Mines / Chicken-Road style round is a client-initiated CLOSE, not a special STEP.

State boundary

RGS owns nothing durable. The wallet adapter is the source of truth for balance, sessions, and cross-round carry. Math state lives in-memory and rebuilds on reconnect from the adapter's stored carry blob. Sessions evaporate on restart and reappear on the next INIT.

What's on disk

hello-spin/
├── package.json          # @open-rgs/core + @open-rgs/contract + your adapter
├── src/index.ts          # one file — boots createServer
├── maths/spin.lua        # one file — your math
├── Dockerfile            # bun, one port, HEALTHCHECK on /readyz
└── .env                  # adapter creds (never committed)