# Moonroll.io Plinko — Independent Fairness & Provably-Fair Audit

## 🏛️ Moonroll.io Plinko — Independent Fairness & Provably-Fair Audit Report

**Date: 13.11.2025**

***

## 1. Executive Summary

This audit evaluates Moonroll.io’s **Plinko** game (<https://moonroll.io/plinko>) across:

* **Provably Fair Cryptographic Fairness**\
  Whether Moonroll implements a proper commit–reveal scheme, seed handling, and deterministic RNG allowing players to independently verify outcomes.

***

### ⭐ Final Verdict

{% hint style="danger" %}
Provably Fair — FAIL

Moonroll does **not** implement any cryptographically verifiable fairness system.

* No **server seed hash**
* No **client seed**
* No **nonce**
* No binding between seeds and outcomes
* The provided “Fairness Checker” is a **simulation** and uses a **different RTP model (95%)**

The Plinko game is **not provably fair**.
{% endhint %}

***

## 1. Provably Fair Evaluation

{% hint style="warning" %}
**Moonroll Plinko is NOT provably fair.**\
The game has *no* cryptographic fairness primitives.
{% endhint %}

***

### 1.1 What Provably Fair SHOULD Look Like

A real commit–reveal system operates as follows:

{% stepper %}
{% step %}

### Server pre-commit

The server generates a random `serverSeed` and publishes `SHA256(serverSeed)` to players before any bets are placed.
{% endstep %}

{% step %}

### Player client seed

The player selects a `clientSeed` (either chosen or random).
{% endstep %}

{% step %}

### Per-bet nonce

For each bet, a `nonce` increments (0, 1, 2, …) to bind each outcome to a unique per-bet index.
{% endstep %}

{% step %}

### RNG derivation

RNG is derived deterministically, e.g.:

RNG = HMAC\_SHA256(serverSeed, clientSeed || nonce)
{% endstep %}

{% step %}

### Reveal and verify

Later, the server reveals `serverSeed`. The player:

* Verifies `SHA256(serverSeed)` matches the published hash.
* Recomputes outcomes from (`serverSeed`, `clientSeed`, `nonce`) for full reproducibility.
  {% endstep %}
  {% endstepper %}

***

### 1.2 What Moonroll Actually Does

From UI, API, HAR, and cookies:

* ❌ **No server seed hash**
  * History only shows a raw “server seed” after bets, with no prior hash.
* ❌ **No client seed**
  * No input in UI, no field in requests or responses, no seed per account.
* ❌ **No nonce**
  * No per-bet index tied to the seed.
* ❌ **No seed material in the bet API**

Bet request:

```json
{
  "betAmount": 0.1,
  "riskLevel": "LOW",
  "numRows": 8
}
```

Bet response already includes the final `path`, `multiplierIndex`, and `multiplier`. No seeds and no hash are exposed.

The server is a **black box RNG**: it computes outcomes internally and returns them fully formed, with no cryptographic transparency.

***

### 1.3 Cookie Analysis (No Seeds Anywhere)

Sample cookies:

```
rolls=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
cf_clearance=...
intercom-device-id-...
intercom-session-...
```

* `rolls` = JWT auth token (user id, email, role, exp).
* `cf_clearance` = Cloudflare anti-bot token.
* `intercom-*` = Intercom chat identifiers.

None of these:

* change per bet, or
* contain seed entropy, or
* are used as client seeds.

| Cookie         | Purpose               | Fairness relevance |
| -------------- | --------------------- | ------------------ |
| `rolls`        | JWT auth              | ❌ None             |
| `cf_clearance` | Cloudflare DDoS token | ❌ None             |
| `intercom-*`   | Support chat tracking | ❌ None             |

***

### 1.4 Plinko Fairness Checker Mismatch

Moonroll provides a **“Plinko Fairness Checker”** with code that:

```js
const combinedSeed = clientSeed + serverSeed;
const random = generateRandomNumber(combinedSeed + i.toString());
```

And constructs multipliers with:

```js
const targetExpectedValue = 0.95; // 95% RTP
```

Problems:

1. **RTP mismatch**
   * Checker forces EV to **95%**.
   * Live game tables are advertised \~**99%** RTP.
2. **Multiplier mismatch**
   * Checker generates a smooth curve with smaller max payouts (e.g. 8.22× extremes for some configs).
   * Live game uses much larger fixed extremes (e.g. 110× and 1000×).
3. **Disconnected from reality**
   * Real bets do **not** use (clientSeed, serverSeed) from this checker.
   * Real API never commits to a seed hash.
   * You cannot plug in a real round and reproduce it.

{% hint style="danger" %}
The “Plinko Fairness Checker” is a **detached simulation**, not a verifier.\
It does not reflect the live game’s payout tables or actual RNG implementation.
{% endhint %}

***

### 1.5 Provably Fair Requirements vs Moonroll Comparison

| Feature / Requirement                     | Proper Provably Fair Game                                   | Moonroll Plinko Implementation                                | Verdict          |
| ----------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------- | ---------------- |
| **Server seed generated by casino**       | Yes – backend generates a random `serverSeed`               | Likely yes (internally), but not exposed                      | ⚠ Opaque         |
| **Server seed hash shown before betting** | Yes – SHA256(serverSeed) shown to player before any bet     | **No** – no commit, hash never shown                          | ❌ Missing        |
| **Client seed chosen by player**          | Yes – player selects clientSeed                             | **No** – no client seed anywhere                              | ❌ Missing        |
| **Per-bet nonce**                         | Yes – increments every bet to ensure uniqueness             | **No** – no nonce in API, UI, cookie, or history              | ❌ Missing        |
| **Deterministic RNG formula**             | Yes – e.g., HMAC\_SHA256(serverSeed, clientSeed \|\| nonce) | **No** – RNG is hidden & not reproducible                     | ❌ Not verifiable |
| **Server seed reveal later**              | Yes – reveal serverSeed so player can replay all past bets  | **No** – reveals a “server seed” string without prior hash    | ❌ Not provable   |
| **Player can replay / verify past bets**  | Yes – all components provided, reproducible                 | **No** – impossible (missing client seed, nonce, and formula) | ❌ Impossible     |
| **Fairness checker matches live game**    | Yes – same multipliers, RTP, and RNG                        | **No** – checker uses 95% RTP & different multipliers         | ❌ Inconsistent   |
| **Checker can reproduce a real bet**      | Yes – matching inputs reproduce same result                 | **No** – inputs do not match real bet requirements            | ❌ Cannot verify  |
| **RTP claims match across system**        | Yes – consistent between UI, docs, and checker              | **No** – live RTP ≈ 99%, checker hardcoded to 95%             | ❌ Contradiction  |
| **Claims of “provably fair” valid**       | Yes – cryptographically verifiable                          | **No** – mechanisms absent                                    | ⚠ Misleading     |

***

## 2. Overall Findings & Recommendations

### 2.1 Weaknesses

* ❌ No server seed hash (no commit).
* ❌ No client seed (no player control).
* ❌ No nonce (no per-bet seed binding).
* ❌ API does not expose any cryptographic data for verification.
* ❌ “Fairness Checker” uses different logic and 95% target RTP.
* ❌ Provably-fair marketing claims are unsupported.

### 2.2 Recommendations

{% stepper %}
{% step %}

### Implement a real commit–reveal system

* Publish `SHA256(serverSeed)` pre-game.
* Reveal `serverSeed` post-rotation.
  {% endstep %}

{% step %}

### Support client seeds

* Allow users to set a client seed.
* Use `serverSeed`, `clientSeed`, `nonce` in the RNG.
  {% endstep %}

{% step %}

### Expose all relevant data

* Include serverSeed hash, clientSeed, and nonce in bet logs.
* Provide a downloadable log or history with these fields.
  {% endstep %}

{% step %}

### Fix the Fairness Checker

* It must use **the exact same multipliers and RTP** as the live game.
* It must reproduce real results from `(serverSeed, clientSeed, nonce)`.
  {% endstep %}
  {% endstepper %}

***

## 3. Final Audit Conclusion

{% hint style="danger" %}
Provably Fairness:

Moonroll’s current implementation **does not meet any accepted definition of a provably fair system.**

There is no commit–reveal process, no client seed, no per-bet nonce, and no deterministic RNG formula that would allow players to independently verify results. The system provides no way to reproduce outcomes, no proof that server seeds were committed before bets, and no linkage between bets and disclosed randomness.

The “Plinko Fairness Checker” presented on their website is not connected to the live game logic. It uses different multipliers, a different RTP model, and a different randomness flow. Because it cannot reproduce real bets, it cannot be considered a verifier.
{% endhint %}

Overall: Moonroll.io Plinko is a **black-box RNG game marketed as “provably fair,”** but without the cryptographic structures required for verifiable fairness.

Regardless of how the internal RTP behaves, the game cannot be independently audited by players, and its fairness claims are unsupported.

***

## 4. Appendix

### 4.1 Plinko Fairness Checker From Official Website

<details>

<summary>Show Plinko Fairness Checker (JS)</summary>

```javascript
// Select the result display element
const resultDiv = document.getElementById('result');

const RISK_LEVELS = {
  LOW: {
    maxMultiplier: 10,
    minMultiplier: 0.3,
    riskFactor: 1.2,
  },
  MEDIUM: {
    maxMultiplier: 33,
    minMultiplier: 0.3,
    riskFactor: 1.5,
  },
  HIGH: {
    maxMultiplier: 1000,
    minMultiplier: 0.2,
    riskFactor: 9.5,
  },
};

const error = message => {
  resultDiv.innerHTML = `<div class="text-red-500 text-center">${message}</div>`;
};

const generateRandomNumber = (seed) => {
  const hash = CryptoJS.SHA256(seed).toString();
  const decimal = parseInt(hash.slice(0, 8), 16);
  return decimal / 0xffffffff;
};

function calculatePositionProbabilities(nbRows) {
  const nbBuckets = nbRows + 1;
  const probabilities = new Array(nbBuckets).fill(0);
  const totalPaths = Math.pow(2, nbRows);

  for (let path = 0; path < totalPaths; path++) {
    let position = 0;
    for (let row = 0; row < nbRows; row++) {
      if ((path & (1 << row)) !== 0) {
        position++;
      }
    }
    probabilities[position]++;
  }

  return probabilities.map((p) => p / totalPaths);
}

function generateMultipliers(riskLevel, nbRows) {
  const config = RISK_LEVELS[riskLevel];
  const nbBuckets = nbRows + 1;
  const multipliers = new Array(nbBuckets);
  const probabilities = calculatePositionProbabilities(nbRows);

  const centerPosition = nbBuckets % 2 === 0
    ? Math.floor((nbBuckets - 1) / 2)
    : Math.floor(nbBuckets / 2);

  for (let i = 0; i <= centerPosition; i++) {
    const distanceFromCenter = Math.abs(i - centerPosition);
    const normalizedDistance = distanceFromCenter / centerPosition;

    if (i === centerPosition) {
      multipliers[i] = config.minMultiplier;
    } else {
      const progressionFactor = Math.pow(normalizedDistance, config.riskFactor);
      multipliers[i] = config.minMultiplier + (config.maxMultiplier - config.minMultiplier) * progressionFactor;
    }
  }

  for (let i = centerPosition + 1; i < nbBuckets; i++) {
    multipliers[i] = multipliers[nbBuckets - i - 1];
  }

  const expectedValue = multipliers.reduce((sum, mult, i) => sum + mult * probabilities[i], 0);
  const targetExpectedValue = 0.95;
  const adjustmentFactor = targetExpectedValue / expectedValue;

  return multipliers.map(mult => Math.round(mult * adjustmentFactor * 100) / 100);
}

function calculateResult(seed, riskLevel, nbRows) {
  let position = 0;
  const path = [];

  for (let i = 0; i < nbRows; i++) {
    const random = generateRandomNumber(seed + i.toString());
    if (random < 0.5) {
      position += 1;
      path.push(1);
    } else {
      path.push(0);
    }
  }

  const multipliers = generateMultipliers(riskLevel, nbRows);
  return {
    multiplier: multipliers[position],
    position: position,
    path: path
  };
}

document.getElementById('verify').onclick = () => {
  const serverSeed = document.getElementById('serverSeed').value;
  const clientSeed = document.getElementById('clientSeed').value;
  const numRows = parseInt(document.getElementById('numRows').value);
  const riskLevel = document.getElementById('riskLevel').value;

  if (!serverSeed) return error('Server seed is required');
  if (!clientSeed) return error('Client seed is required');
  if (!numRows || numRows < 8 || numRows > 16) return error('Number of rows must be between 8 and 16');
  if (!RISK_LEVELS[riskLevel]) return error('Invalid risk level');

  try {
    const combinedSeed = clientSeed + serverSeed;
    const gameResult = calculateResult(combinedSeed, riskLevel, numRows);
    
    resultDiv.innerHTML = `
      <div class="text-center">
        <table class="w-full border-collapse border border-gray-600">
          <thead>
            <tr>
              <th class="border border-gray-600 p-2">Position</th>
              <th class="border border-gray-600 p-2">Multiplier</th>
              <th class="border border-gray-600 p-2">Path</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td class="border border-gray-600 p-2">${gameResult.position}/${numRows}</td>
              <td class="border border-gray-600 p-2">${gameResult.multiplier}x</td>
              <td class="border border-gray-600 p-2">${gameResult.path.join(' → ')}</td>
            </tr>
          </tbody>
        </table>
      </div>`;
  } catch (e) {
    console.error('Error:', e);
    error('An error occurred while generating the results.');
  }
};
```

</details>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://provablyfair.org/moonroll-plinko-audit/moonroll.io-plinko-independent-fairness-and-provably-fair-audit.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
