A GLI-19 certificate proves the random number generator was fair on the day the laboratory tested it. It says nothing about the build running tonight.
Most conversations about fairness in iGaming stop at the certificate. An independent test house samples the RNG, runs a battery of statistical tests, signs a report, and the operator files it. Everyone treats the framed PDF as if it were a live guarantee. It is not. It is a point-in-time attestation about a specific binary in a specific configuration, and the moment you deploy a new game build, retune a paytable, patch a library or reseed under load, the certified thing and the running thing have quietly diverged. The question a Maltese regulator is increasingly likely to ask is not “do you hold a certificate” but “how would you know, tonight, if the RNG or the return-to-player had drifted.”
What the certificate actually proves
GLI-19, the Gaming Laboratories International standard for interactive gaming systems, is the reference most European operators are tested against; the current edition is version 3.0, released in July 2020. Its RNG section is careful and worth reading properly rather than in summary. It requires an accredited laboratory to apply a suite of statistical tests to RNG output — total distribution or chi-square, runs, overlaps, coupon-collector, serial correlation and others — and to evaluate them collectively at a 99% confidence level over a data set large enough to detect meaningful deviation. For hardware-based generators it goes further and expects dynamic monitoring of output, with game play disabled on detected degradation.
Free · 4 minutes
Is your engineering team shipping safely, or quietly accumulating risk?
Fourteen questions on how work gets from idea to production — cadence, testing, rollback, and the key-person risk in your delivery. Banded finding on screen, full sheet by email.
That is a strong test. The problem is temporal, not methodological. The laboratory runs it once, against a submitted build, in controlled conditions. Certification proves the generator was sound at submission. It cannot prove the generator is sound after the fourteenth hotfix of the quarter, and no one issues a fresh certificate for every deployment. The gap between what was certified and what is live is exactly the gap a bad build, a botched merge or a tampered seed lives in. If you want the full argument for reading these certificates as evidence rather than decoration, I have written about the technology-audit reading of RNG and RTP verification separately.
The same tests, run against the live stream
The move to continuous assurance is not exotic. It is running the laboratory’s own tests on a rolling window of production output, on your own infrastructure, and alerting when they fail. Two tests carry most of the load. A chi-square goodness-of-fit test asks whether the distribution of scaled RNG draws is uniform across its range — the check that catches a generator that has started favouring some outcomes over others. A runs test asks whether the sequence is independent — the check that catches structure, periodicity or a stuck bit that a distribution test alone would miss because the marginal frequencies still look fine.
Alongside the raw RNG, you monitor the money. Return-to-player is the business-facing consequence of the generator plus the paytable, and it is the number a player, a regulator and a fraud team all care about. Observed RTP over a large enough window should sit close to the game’s theoretical figure. Sustained drift below it can mean a paytable error, a truncation bug or deliberate manipulation; sustained drift above it usually means a costly configuration mistake. Malta lowered the minimum permitted RTP for remote games from 92% to 85% in 2021 under its Player Protection Directive, but the licensed figure that matters for monitoring is the one certified for each specific game, not the floor.
The following is a compact, standard-library implementation of the three checks over a window of live rounds. It uses a chi-square lookup for clarity; in production, compute critical values with scipy.stats.
"""Continuous RNG/RTP assurance over a rolling window of live rounds.
Standard library only; British spelling throughout."""
import math
from collections import Counter
from dataclasses import dataclass
# Chi-square critical values at the 99% level (alpha = 0.01), keyed by
# degrees of freedom. In production use scipy.stats.chi2.ppf(0.99, df).
CHI2_CRIT_99 = {9: 21.666, 15: 30.578, 31: 52.191, 63: 92.010}
@dataclass
class Round:
draw: float # RNG output normalised to [0, 1)
stake: float
payout: float
@dataclass
class Verdict:
metric: str
statistic: float
threshold: float
breached: bool
def chi_square_uniform(draws, buckets=16):
"""Goodness-of-fit against a uniform distribution over `buckets` bins."""
counts = Counter(min(int(x * buckets), buckets - 1) for x in draws)
expected = len(draws) / buckets
stat = sum((counts.get(b, 0) - expected) ** 2 / expected
for b in range(buckets))
crit = CHI2_CRIT_99[buckets - 1]
return Verdict("chi_square", stat, crit, stat > crit)
def runs_test(draws):
"""Wald-Wolfowitz runs test on a median split; two-tailed z at 99%."""
median = sorted(draws)[len(draws) // 2]
signs = [x >= median for x in draws]
n1, n2 = sum(signs), len(signs) - sum(signs)
if n1 == 0 or n2 == 0:
return Verdict("runs", 0.0, 2.576, False)
runs = 1 + sum(1 for a, b in zip(signs, signs[1:]) if a != b)
mean = 1 + (2 * n1 * n2) / (n1 + n2)
var = (2 * n1 * n2 * (2 * n1 * n2 - n1 - n2)) / \
((n1 + n2) ** 2 * (n1 + n2 - 1))
z = abs(runs - mean) / math.sqrt(var)
return Verdict("runs", z, 2.576, z > 2.576)
def rtp_drift(rounds, theoretical_rtp, tolerance=0.005):
"""Observed vs certified RTP; tolerance is a starting band, not gospel."""
staked = sum(r.stake for r in rounds)
observed = sum(r.payout for r in rounds) / staked
drift = observed - theoretical_rtp
return Verdict("rtp_drift", drift, tolerance, abs(drift) > tolerance)
def assess(window, theoretical_rtp):
draws = [r.draw for r in window]
verdicts = [chi_square_uniform(draws), runs_test(draws),
rtp_drift(window, theoretical_rtp)]
for v in verdicts:
if v.breached:
print(f"ALERT {v.metric}: {v.statistic:.4f} "
f"breaches {v.threshold}")
return verdicts
Setting thresholds you can defend
Two decisions turn this from a demo into a control: the window and the band. The window has to be large enough that the statistics are meaningful — a chi-square over a few hundred rounds is noise — but short enough that a fault surfaces in hours, not weeks. Tie it to volume rather than wall-clock time: assess every N completed rounds per game, and hold a rolling buffer sized so that expected counts per bucket stay comfortably above five. The RTP band is the harder call. A fixed half-a-percent tolerance is a reasonable place to start, but a high-variance slot legitimately swings wider over any given window than a low-variance one, so a defensible programme derives the band from each game’s declared variance rather than applying one number across the catalogue. Alert on sustained breach across consecutive windows, not a single excursion, or you will train your team to ignore the pager.
This is a change-governance control, not a maths exercise
The reason continuous RNG assurance belongs to change governance rather than to the data-science backlog is that its whole value is catching what changes. The dangerous events are deployments: a new game version, a shared library bump, an infrastructure migration that alters how the generator is seeded. The monitor earns its keep at exactly those moments, which means it has to be wired to your release pipeline. Every deployment that touches a game or the RNG path should carry a change record, and the assurance dashboard should let you overlay breaches against that record — so when chi-square goes red at 02:00, the first question, “what shipped just before this,” already has an answer. If you are already instrumenting deployment frequency and change-failure rate, RNG drift is simply another change-failure signal with a statistical trigger. And the cheapest breach is the one that never reaches production: the same tests belong in a pre-deployment gate, part of a CI baseline that actually runs, so a build whose output distribution has shifted fails the pipeline before a player ever sees it.
The evidence a supervisor will ask for
Live monitoring is only worth having if it produces retained, reconstructable evidence. Sampled raw draws, the test statistics per window, the thresholds in force at the time, every alert with its disposition, and the change record it was correlated against — all held long enough to answer a question raised months later. That turns “we hold a GLI-19 certificate” into “we hold the certificate, and here is the continuous record showing the certified behaviour held between certifications, plus the three occasions it wobbled and what we did.” The first is a document on a wall. The second is a control that operates. A regulator who has learned to distinguish the two — and Malta’s has — will notice which one you have brought.
The certificate tells you the game was fair the day it was tested. Only the monitor tells you it is fair while someone is losing money on it right now.
Most technology problems are not technology problems. They are control problems.
The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.