You can extend...
Five plug points. Each is one interface in
@open-rgs/contract; pick the smallest one that
covers the change you want.
Slot libraries
Math is TypeScript, so a library is an ordinary package you import. There is nothing to register.
import { rect } from "@open-rgs/grid";
import { stackyFill } from "@open-rgs/markov";
import { evalLines, rowLines } from "@open-rgs/pay-lines"; Every extension point is one of three signatures, so writing your own means writing a function. See the library set.
Generator<S> = (next) => Grid<S> // makes a board
Selector<S> = (grid, next) => Pos[] // picks cells
Effect = (grid, pos, next) => Grid // changes a board 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.ts --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 { uuidV4 } from "@open-rgs/core";
// `generate` is the random fallback for round-INITIATING calls (spin/open)
// with no client token. Close/autoclose keys are derived deterministically
// from (sessionId, roundId) - see deriveIdempotencyKey.
await createServer({
idempotency: { generate: uuidV4, ttlMs: 600_000 },
manifest, platform, transport,
});