You can extend…
Five plug points. Each is one interface in
@open-rgs/contract; pick the smallest one that
covers the change you want.
The Lua VM (helpers, DSLs, native fast paths)
A LuaExtension is one of three things, often
combined: a pure-Lua module returned by
require("<name>"), a table of native (TS)
helpers merged into that module, or a pre-evaluation source
transform applied to every loaded file.
// boot file — install an extension
import { loadLuaMath } from "@open-rgs/core";
import reels from "@open-rgs/ext-reels";
const math = await loadLuaMath("./maths/spin.lua", { extensions: [reels] }); // write your own extension
import type { LuaExtension } from "@open-rgs/contract";
export default {
name: "easing",
version: "0.1.0",
lua: `
local M = {}
function M.lerp(a, b, t) return a + (b - a) * t end
return M
`,
host: () => ({
fast_pow: (x: number, n: number) => x ** n, // native, hot-path
}),
} satisfies LuaExtension; -- maths/spin.lua — math sees the extension via require
local easing = require("easing")
local y = easing.lerp(0, 100, host.rng_next())
local p = easing.fast_pow(1.05, 7)
Reference extension:
@open-rgs/ext-reels
— strip generation, payline evaluation, book-of utilities.
Deeper: math reference.
The wallet
Implement PlatformAdapter. How you talk to the
operator (one WS, three microservices, REST + polling, gRPC) is
your call — the orchestrator only sees the interface.
import type { PlatformAdapter } from "@open-rgs/contract";
export class MyAdapter implements PlatformAdapter {
isHealthy = false;
diagnostics = {};
async connect() { /* open WS / dial / login */ this.isHealthy = true; }
disconnect() { /* tear down */ }
async openSession(sid, conn) { /* GET /sessions/:sid → SessionInfo */ }
async settleSimple(req) { /* POST /play → RoundReceipt */ }
async openComplex(req) { /* POST /open → RoundReceipt */ }
async closeComplex(req) { /* POST /close → RoundReceipt */ }
onEvent(handler) { /* wire WS push → handler(event) */ }
}
Helpers: @open-rgs/adapter-kit for WS/HTTP RPC
scaffolding, error mapping, diagnostics.
Conformance: @open-rgs/adapter-test-kit runs the
seven RPCs + event stream against your adapter.
Deeper: adapter reference.
The transport
Implement ClientTransport. The default is
binaryTransport — binary-msgpack over WebSocket. You
could write a JSON transport, HTTP long-poll, gRPC, anything
that produces typed OrchestratorAPI calls.
import type { ClientTransport, OrchestratorAPI } from "@open-rgs/contract";
export function jsonTransport(opts: { port: number }): ClientTransport {
return {
async start(api: OrchestratorAPI) {
// dispatch incoming JSON frames into api.init / api.spin / api.openRound / ...
return { port: opts.port };
},
stop(o) { /* drain + close */ },
setExtraFetch(fn) { /* mount admin handler on the same Bun.serve */ },
};
}
Implementing setExtraFetch lets
createServer mount /admin/* and probes
on your transport's port (single-port mode). Skip it and the
caller passes adminPort to get a separate listener.
Deeper: wire reference.
Metrics & logs
Bring your own RgsMetrics registry, or use the
standard Prometheus one. Bring your own log formatter via
@open-rgs/log (bundled: json,
console, server-core).
import { createRgsMetrics } from "@open-rgs/core";
import { log } from "@open-rgs/log";
log.setFormat("json"); // or "console" / "server-core" / a custom fn
await createServer({
metrics: createRgsMetrics({ prefix: "myco_" }),
manifest, platform, transport,
});
Metrics surface at GET /admin/metrics. Ring-buffered
logs at GET /admin/logs?level=&limit=.
Deeper: admin reference.
Simulator marks & expectations
Annotate the math with host.mark.* calls and a
expected block. The orchestrator ignores both; the
simulator records them and reports deviations vs target.
return {
kind = "simple", name = "spin", version = "0.1.0", rtp = 0.95,
expected = {
hitRate = { target = 0.32, tolerance = 0.01 },
rtpContribution = {
scatter = { target = 0.18 },
paylines = { target = 0.77 },
},
},
play = function(prev, ctx)
local m = roll()
if m > 0 then host.mark.tag("win") end
host.mark.contribute("paylines", m * 0.9)
host.mark.contribute("scatter", m * 0.1)
return { multiplier = m, ops = { ... }, type = m > 0 and "win" or "loss" }
end,
}
Marks are inert in production — zero cost. Run a sim:
bunx open-rgs-sim ./maths/spin.lua --spins 1e8.
Reports: per-mode JSON, Markdown, HTML.
Modes (ante, buy, freespins)
Multi-mode games are a manifest concern, not a math concern. Add
a mode with a stakeMultiplier, mark it
internal: true if only reachable via
next_mode, give it its own declaredRtp
and optional maxWinMultiplier.
Deeper: boot reference.
Idempotency
Configure the key generator + TTL at boot. Default is uuid-v4 with a 5-minute dedupe window. The key is forwarded to every state-changing wallet RPC; the wallet may dedupe.
import { cuidV2 } from "@open-rgs/core/idempotency";
await createServer({
idempotency: { generate: cuidV2, ttlMs: 600_000 },
manifest, platform, transport,
});