SDKs
Seven SDKs, one behaviour. Each one:
- signs requests in both forms (Engine body-only and full-request) and produces exactly the signatures in
vectors/signing.json; - calls the operator API (launch, demo, catalogue, round and transaction lookups) and raises a typed error with the HTTP status, code, message and request id;
- verifies the RGS's signed calls on your wallet and builds the wallet's error and balance answers.
They have no third-party dependencies. All are tested against the shared vectors, and all (except that the Rust crate leaves HTTP to you) are tested against a real operator-api by interop/run.sh.
| Language | Directory | Needs | HTTP |
|---|---|---|---|
| TypeScript | typescript/ | Node 22.6+ (runs the .ts directly; compile with tsc for older runtimes) | fetch |
| Python | python/ | Python 3.9+ | urllib |
| Go | go/ | Go 1.22+ | net/http |
| Java | java/ | Java 17+ | java.net.http |
| Rust | rust/ | Rust 1.80+ | none: the crate builds the request, you send it |
| PHP | php/ | PHP 8.1+ with ext-sodium and ext-json | PHP streams |
| C# | csharp/ | .NET 8+ | HttpClient |
For any other language, the signing scheme in Authentication is short enough to implement directly, and the vectors tell you when you have it right.
The same four things in every language
Call the operator API
// TypeScript
const rgs = new OperatorClient({ baseUrl, operatorId: "acme", privateKeyPem, fullRequestSigning: true });
const { url } = await rgs.launch({ product: "runecode", publisher: "wizards", game: "royal-tiger-v2",
user: "player-1", token: crypto.randomUUID(), currency: "USD" });
# Python
rgs = OperatorClient(base_url, "acme", private_key_pem, full_request_signing=True)
url = rgs.launch({"product": "runecode", "publisher": "wizards", "game": "royal-tiger-v2",
"user": "player-1", "token": str(uuid.uuid4()), "currency": "USD"})["url"]
// Go
rgs, _ := operator.NewClient(operator.ClientOptions{BaseURL: base, OperatorID: "acme", PrivateKeyPEM: pem, FullRequestSigning: true})
res, err := rgs.Launch(map[string]any{"product": "runecode", "publisher": "wizards", "game": "royal-tiger-v2",
"user": "player-1", "token": uuid, "currency": "USD"}, "")
// Java
OperatorClient rgs = new OperatorClient(base, "acme", pem, true);
String json = rgs.launch(Map.of("product", "runecode", "publisher", "wizards", "game", "royal-tiger-v2",
"user", "player-1", "token", uuid, "currency", "USD"), null).body();
// PHP
$rgs = new OperatorClient($base, 'acme', $privateKeyPem, true);
$res = $rgs->launch(['product' => 'runecode', 'publisher' => 'wizards', 'game' => 'royal-tiger-v2',
'user' => 'player-1', 'token' => $uuid, 'currency' => 'USD']); // $res['url']
// C#
var rgs = new OperatorClient(baseUrl, "acme", privateKeyPem, fullRequestSigning: true);
var res = await rgs.LaunchAsync(new JsonObject { ["product"] = "runecode", ["publisher"] = "wizards", ["game"] = "royal-tiger-v2",
["user"] = "player-1", ["token"] = uuid, ["currency"] = "USD" }); // res["url"]
// Rust: the crate prepares the call; send it with any HTTP client
let s = Signer::new("acme", &pem)?;
let req = s.prepare("POST", "/v1/game/url", "", body_json.as_bytes(), true, now_unix_secs);
// send req.method to base + req.path_and_query with req.headers and req.body
Other calls: products(), games(product, after?), launchDemo(...), bet(id), roundsGet(selector), transactionsGet(selector) (snake_case in Python; LaunchDemo, RoundsGet, … in Go; Rust prepares any of them with prepare).
Handle errors
A non-2xx answer raises RgsError (TypeScript, Python), returns *RgsError (Go) or throws RgsException (Java), with status, code, message and requestId. Branch on status first. Keep requestId in your logs.
Verify a wallet call from the RGS
const check = verifyWalletRequest(rawBody, req.headers, { operatorSlug: "acme", rgsPublicKeys: [current, standby] });
if (!check.ok) return reply(check.error.response()); // 401 { code: "ERR_ATE", message: … }
verify_wallet_request(raw_body, headers, "acme", [current, standby]) # raises WalletError("ERR_ATE")
if e := operator.VerifyWalletRequest(raw, r.Header, "acme", []string{current, standby}); e != nil { /* e.Status(), e.Body() */ }
WalletError.verifyRequest(rawBody, headers, "acme", List.of(current, standby)); // throws WalletError
verify_wallet_request(raw_body, x_operator, x_signature, "acme", &[current, standby])?;
WalletError::verifyRequest($rawBody, $headers, 'acme', [$current, $standby]); // throws WalletError
WalletError.VerifyRequest(rawBody, headers, "acme", new[] { current, standby }); // throws WalletError
Always pass both RGS keys, and pass the raw bytes, not a re-serialized object.
Answer a wallet call
Use the helpers for the balance body (balance(amount, currency)) and for errors (WalletError(code, message) with the right HTTP status). The full contract is in Implementing the wallet; the reference wallet shows all of it together.
Verifying an SDK yourself
node --test typescript/test/ # TypeScript
python3 -m unittest discover -s python/tests -t python
(cd go && go test ./...)
(cd java && javac -d out src/main/java/io/runecode/operator/*.java src/test/java/io/runecode/operator/*.java && java -cp out io.runecode.operator.VectorTests)
(cd rust && cargo test)
php php/tests/run.php # PHP (docker run --rm -v "$PWD":/w -w /w/php php:8.3-cli-alpine php tests/run.php if PHP is not installed)
(cd csharp && dotnet run --project tests -c Release) # C# (or the dotnet/sdk:8.0 image; see interop/run.sh)
interop/run.sh # every SDK against a real operator-api (needs a Core checkout)