A two-party commit-reveal RNG for casino and game operators. Your server and the RAIN node each lock in a secret before either sees the other’s, so neither side can bias the outcome. Every spin ships with a /verify link any player can check.
import { NodeClient } from "@rain/rng-node"; import { drbg } from "@rain/rng-core"; const rng = new NodeClient({ baseUrl: "https://api.rainrng.xyz", apiKey: process.env.RAIN_API_KEY, // server-side only operatorPubKey: NODE_PUBKEY, // pin it }); await rng.open("player-42"); const round = await rng.round(JSON.stringify({ bet: 1 })); const reels = drbg(round.gameSeed("slot-v1"), "slot-v1").cursor(); [reels.intBelow(20), reels.intBelow(20), reels.intBelow(20)]; // → [7, 13, 2] proof: await rng.verifyUrl(round.k)
Read live from api.rainrng.xyz when this page loads. These endpoints are public and need no key.
Each side commits to its secret before it can see the other’s. The result of a round is fixed only when both reveals meet. The house can’t pick a seed that favours it, and your server can’t either.
The node publishes houseSeedCommit and the root of a hash chain of 4,096 rounds, all signed with its ed25519 key.
After seeing the terms, your server sends its own seed and chain root. The session seed mixes both: keccak(houseSeed, playerSeed, sessionId).
Your server reveals its chain link for round k. The node checks it, persists to Postgres before replying, then reveals its own link.
The round value r_k seeds an SP 800-90A HMAC-DRBG (or ChaCha20). Outcomes use unbiased rejection sampling. Every round gets a public proof.
You generate the API key yourself and send us only its SHA-256 fingerprint. The node stores fingerprints only, so the key itself never leaves your servers.
# generate on YOUR server — keep $KEY in your secret store KEY=$(openssl rand -hex 32) # send RAIN only the fingerprint (64 hex chars) echo -n "$KEY" | sha256sum # → f2e9…5fb3 (cannot be reversed into the key)
# once RAIN confirms your fingerprint is loaded: curl -s -o /dev/null -w "%{http_code}\n" \ -X POST https://api.rainrng.xyz/v2/terms \ -H "authorization: Bearer $KEY" \ -H "content-type: application/json" -d '{}' # 200 → connected 401 → key/fingerprint mismatch
import { NodeClient } from "@rain/rng-node"; const c = new NodeClient({ baseUrl: "https://api.rainrng.xyz", transport: "ws", // or "http" apiKey: process.env.RAIN_API_KEY, operatorPubKey: process.env.RAIN_NODE_PUBKEY, }); const sid = await c.open("session-label"); // pipelining: commit k+1 while the reels of k animate
# every protected call carries the same header Authorization: Bearer <RAIN_API_KEY> # WebSocket: header on upgrade, or first frame wss://api.rainrng.xyz/v2/ws {"type":"auth","key":"<RAIN_API_KEY>"} # errors 401 unauthorized 403 other operator's session 429 rate limited (honor Retry-After) 503 self-test failed
/verify is public./verify/:sid/:k under a “Powered by RAIN RNG” badge.JSON over HTTPS, with the same envelopes over WebSocket. Base URL https://api.rainrng.xyz.
| method | path | access | description |
|---|---|---|---|
| POST | /v2/terms | api key | Node-signed terms: houseSeedCommit, houseChainRoot, chain length. |
| POST | /v2/open/:termsId | api key | Operator seed and chain root; opens the session and binds it to your operator id. |
| POST | /v2/reveal | api key | Commit for round k. The node persists, then returns hRev_k. Idempotent on replay. |
| GET | /v2/reveal/:sid/:k | api key | Re-fetch an already-revealed round, e.g. after a client crash. |
| WS | /v2/ws | api key | The same operations as JSON envelopes over one connection, for the lowest latency. |
| GET | /verify/:sid/:k | public | Proof for one round: both reveals, the session seed and r_k. CORS *. |
| GET | /healthz | public | Liveness, store, auth mode, rate limit, self-test state. |
| GET | /selftest | public | Runs known-answer tests now. Any failure switches output off (503) until fixed. |
| GET | /metrics | public | Prometheus text: rounds, latency, 401/403/429 per operator. |
| GET | / | public | Node identity: operator id, ed25519 public key, chain length, endpoint list. |
Paste a proof link, or a session id and round number. This page fetches the proof straight from the node; nothing passes through us.
Built so that a bug, a crash or a bad actor on either side shows up in the proof and can’t pass silently.
Each side draws 256 bits from the OS CSPRNG and commits to them first. A seed is never taken from the clock or a fixed value, or set by one side alone.
Every reveal is written to Postgres before the node answers. After a restart or a client crash, each round re-fetches identically and can never be redrawn.
Terms are signed with the node’s ed25519 key. Clients pin the public key, so a spoofed node is rejected.
Keys are stored as SHA-256 hashes only and compared in constant time. A session belongs to the operator that opened it. Any other operator gets 403.
Each operator gets its own token bucket, 429 with Retry-After, and its own metrics.
Known-answer tests for keccak, SHA-256, HMAC-DRBG and ChaCha20 run at startup, every 24 h and on demand. If one fails, the node stops issuing rounds.
Our own pre-testing, run on the same code the node runs, and organised to the GLI-19 v3.0 structure.
Dieharder 3.31.1 full battery, stream input. ChaCha20 90 passed / 5 weak; HMAC-DRBG 83 passed / 3 weak.
NIST SP 800-22 result lines in range, for each mechanism (1,000 × 10⁶ bits).
TestU01 SmallCrush statistics passed, for both mechanisms.
Scaled game outcomes per game (dice, roulette, coin, reels) checked with χ², KS, serial correlation and runs tests.
One spin over WAN, sequential, coast-to-coast path (~95 ms RTT). 0 errors in 4,000 rounds.
Same path with WebSocket and pipelining, at a harsh 60 ms animation.
With a real reel animation of 1 s or more, the reveal is already waiting when the player looks.