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,
});