INTRODUCTION

$DICE Documentation

$DICE is a faithful Solana port of Just-Dice.com, the 2013 Bitcoin dice game built by dooglus on BitcoinTalk. Same provably fair HMAC-SHA512 algorithm. Same “be the house” mechanic. Now on a chain where it can actually scale.

KEY PROPERTIES
Provably fairHMAC-SHA512 with rejection sampling. Verifiable using dooglus's open-source verifier.
Zero gameplay sell pressureLosses recycle to the house pool. Tokens are never market-sold from gameplay.
Deflationary1% of every bet is burned permanently.
Off-chain ledgerBets are instant Redis updates. Deposits/withdrawals settle on Solana.
Custodial vaultSame trust model as OG Just-Dice (held 50,000 BTC). On-chain proof of solvency.
Wallet sig authEvery game action requires an Ed25519 signature.
CONTEXT

The Story

In June 2013, dooglus posted on BitcoinTalk about a dice game he'd built. It had a 1% house edge, provably fair rolls, and one novel feature: anyone could deposit Bitcoin to the bankroll and earn a proportional cut of house profits. At its peak it held ~50,000 BTC (~$50M then, ~$4.5B today) — more than most exchanges of the era.

In 2014, regulators tightened. dooglus migrated the entire operation to CLAM, an obscure proof-of-stake coin, to dodge jurisdictional risk. The site is still running thirteen years later — same dooglus, same algorithm, stranded on a chain with no market.

$DICE brings it back where it can actually scale.

2013
BITCOIN era. dooglus launches Just-Dice on BTT. Bankroll grows to 50,000 BTC. First crypto liquidity pool.
2014
EXILE. Regulatory pressure forces migration off Bitcoin. Site moves to CLAM. Same algorithm, ghost chain.
2026
SOLANA. $DICE brings the game back to a chain with a market. Same HMAC-SHA512, 400ms settlement, on-chain settlement of deposits/withdrawals.
SYSTEM

Architecture

The system has four components: a Next.js frontend, serverless API routes, an Upstash Redis ledger for off-chain state, and a Solana vault wallet that custodies all deposited tokens.

System Architecture

Why off-chain balance tracking?

The OG Just-Dice handled millions of bets. On-chain settlement per bet would be impossibly slow and expensive even on Solana. The standard pattern for high-frequency on-chain games (and CEXs): users deposit once, play many times, withdraw when done.

Deposit 1,000 $DICE   →  1 on-chain transaction
Play 200 bets         →  0 on-chain transactions  (Redis only)
Withdraw 1,847 $DICE  →  1 on-chain transaction

Solvency invariant

The vault is solvent by design. At any time:

vault.tokenBalance ≡ Σ(player.balance) + housePool + totalBurned

Every $DICE in the system was deposited by someone.
Every $DICE leaving the vault corresponds to a withdrawal.
The 1% edge is accounted for in totalBurned but stays in the vault as house liquidity.
PROTOCOL

Game Mechanics

Placing a bet

A bet is parameterized by:

  • target — a number from 0.01 to 99.98
  • rollHigh — boolean. If true, you win when roll > target. If false, when roll < target.
  • betAmount — your wager in $DICE raw lamports (6 decimals)

The chance and multiplier:

chance     = rollHigh ? (99.99 - target) : target
multiplier = 99 / chance     // 1% house edge baked in

Example: target = 49.50, rollHigh = truechance = 50.49%, multiplier = 1.9608×. A 100 $DICE bet has a potential payout of 196.08 (profit 96.08) with ~50% probability.

Bet Lifecycle

Settlement

OutcomeEffect
Winbalance += payout, housePool -= profit, burned += betAmount × 1%
Losebalance -= betAmount, housePool += betAmount × 99%, burned += betAmount × 1%

Limits

Min bet0.001 $DICE (1,000 raw)
Min withdraw0.01 $DICE (10,000 raw)
Max profit0.5% of housePool (dynamic)
Min chance0.01%
Max chance99.98%

Jackpot

A separate SOL pool, funded by creator fees from DEX trading volume. Triggered by rolling exactly 77.77 on any bet. 80% paid to the winner, 20% reseeds the next jackpot. Independent from the dice economy — pure bonus prize.

CRYPTOGRAPHY

Provably Fair Algorithm

This is the exact algorithm used by the original just-dice.com (post bet ID 145M). HMAC-SHA512 with rejection sampling to eliminate modulo bias.

Algorithm Flow

Implementation

// lib/dice.ts
import crypto from "crypto";

export function computeRoll(
  serverSeed: string,
  clientSeed: string,
  nonce: number
): { roll: number; hash: string } {
  // Post-145M format: nonce wraps both message and key
  const message = `${nonce}:${clientSeed}:${nonce}`;
  const key = `${nonce}:${serverSeed}:${nonce}`;
  const hash = crypto.createHmac("sha512", key).update(message).digest("hex");

  // Rejection sampling: take 5 hex chars at a time
  for (let i = 0; i < 25; i++) {
    const chunk = hash.substring(5 * i, 5 * i + 5);
    const value = parseInt(chunk, 16);
    if (value < 1_000_000) {
      return { roll: value / 10000, hash };
    }
  }

  // Fallback (extremely rare): last 3 hex chars
  const last3 = hash.substring(125, 128);
  return { roll: parseInt(last3, 16) / 10000, hash };
}

Why rejection sampling?

parseInt(hex5chars, 16) produces values from 0 to 1,048,575 (16⁵ − 1). If we used value % 10000, results 0–48,575 would be slightly more likely than 48,576–9,999 — a tiny but real bias. Instead we reject any value ≥ 1,000,000 and try the next 5 hex characters. 96% of rolls succeed on the first attempt; the bias is mathematically zero.

Verification flow

  1. Before any bet, the client fetches /api/balance which returns seedHash = SHA256(server_seed). The actual server_seed is never revealed.
  2. Player bets. Each bet uses an incrementing nonce. The client_seed defaults to "default" but can be set to anything.
  3. Player can rotate seeds at any time via /api/seeds. This reveals the previous server_seed, generates a new one, and returns the new seedHash.
  4. Player verifies that SHA256(revealedServerSeed) === oldSeedHash, then replays each bet using (revealedServerSeed, clientSeed, nonce) in any independent verifier.

Third-party verifiers

Built by the OG Just-Dice community in 2013, still online and compatible with our rolls:

ECONOMICS

Tokenomics & Flywheel

Flywheel

Value capture

ForceDirectionSource
Buy pressure↑ PricePlayers must buy $DICE to play
Burn↓ Supply1% of every bet, permanently destroyed
Loss recyclingNeutralLost $DICE → house pool, never sold
Creator fees→ SOL JackpotFunds bonus prize pool, grows with volume

The economic loop

  1. Player wants to play → must buy $DICEbuy pressure
  2. Player bets → 1% burned → deflationary
  3. Player wins → gets $DICE from house pool (no chart impact)
  4. Player loses → $DICE goes to house pool (no chart impact)
  5. Trading volume generates pump.fun creator fees → accumulates as SOL jackpot
  6. Jackpot grows → attracts more players → loop repeats
REFERENCE

API Reference

All endpoints are Next.js API routes under /api/*. Game endpoints require Bearer auth.

GET /api/auth?wallet=<pubkey>

Returns a nonce for the wallet to sign.

{
  "nonce": "Sign this message to authenticate with Just-Dice.\n\nWallet: <pubkey>\nTimestamp: <ms>"
}

POST /api/auth

Verifies signature, issues session token (24h TTL).

// request
{ "wallet": "<pubkey>", "signature": "<base58 signature>" }

// response
{ "token": "<64-char hex>" }

GET /api/balance?wallet=<pubkey>

Returns player state and global stats. No auth required.

{
  "balance": "1000000000",         // raw, 6 decimals
  "seedHash": "<sha256 hex>",
  "clientSeed": "default",
  "nonce": 0,
  "housePool": "1000000000000000",
  "totalBurned": "60000",
  "jackpot": "500000000",          // SOL lamports
  "totalWagered": "...",
  "totalBets": 6,
  "maxPayout": "5000000000000",
  "recentBets": [...]
}

POST /api/game

Place a bet. Requires Authorization: Bearer <token>.

// request
{
  "wallet": "<pubkey>",
  "target": 49.5,
  "rollHigh": true,
  "betAmount": "100000000"   // raw, 6 decimals
}

// response
{
  "roll": 72.4137,
  "won": true,
  "target": 49.5,
  "rollHigh": true,
  "chance": 50.49,
  "multiplier": 1.9608,
  "betAmount": "100000000",
  "payout": "196078431",
  "profit": "96078431",
  "burned": "1000000",
  "balance": "1096078431",
  "hash": "<128 hex chars>",
  "seedHash": "<sha256 hex>",
  "nonce": 1,
  "jackpotWon": false,
  "jackpotAmount": "0"
}

POST /api/seeds

Rotate server seed (reveals previous), optionally set new client seed. Requires auth.

// request
{ "wallet": "<pubkey>", "clientSeed": "my-new-seed" }

// response
{
  "previousServerSeed": "<64-char hex>",
  "previousNonce": 4,
  "newSeedHash": "<64-char hex>",
  "clientSeed": "my-new-seed"
}

POST /api/deposit

Verify an on-chain SPL transfer to the vault and credit the player's Redis balance.

// request
{ "wallet": "<pubkey>", "signature": "<solana tx sig>" }

// response
{ "credited": "1000000000", "balance": "1000000000", "signature": "..." }

POST /api/withdraw

Send $DICE from the vault to the player's wallet. Requires auth.

// request
{ "wallet": "<pubkey>", "amount": "100000000" }

// response
{ "success": true, "amount": "100000000", "balance": "0", "signature": "..." }
SECURITY

Security Model

ThreatMitigation
Bet as another walletEd25519 signature auth — every game request requires a session token tied to a signed nonce.
Replay old auth5-minute nonce TTL — auth challenges expire if not signed.
Race condition on betsRedis distributed lock (jd:lock:bet:<wallet>, 10s TTL). Concurrent bets return HTTP 429.
Race condition on withdrawalsSame lock pattern. Failed transactions trigger atomic balance restore.
Double-credit depositsTransaction signatures stored with 30-day TTL. Already-credited txs return HTTP 409.
Vault drain by API bugWithdrawal debits balance before sending tx. If tx fails, balance is restored.
Rigged rollsProvably fair — pre-committed server seed hash + user-chosen client seed + open-source verifier.
Server seed manipulationServer seed is generated server-side and never exposed until rotated. SHA-256 commitment is published before any bet uses it.

Trust assumptions

The vault is custodial. The keypair controls all deposited $DICE. This is the same trust model as the OG Just-Dice (which custodied 50,000 BTC successfully for years), and the same as any centralized exchange. Mitigations:

  • Vault on-chain balance is publicly visible. Anyone can verify solvency.
  • Withdrawal endpoint is automated — no manual approval, can't be censored.
  • Provably fair guarantees we can't rig outcomes.

This is a deliberate tradeoff for UX: instant bets, no per-bet gas, no smart contract risk. A future v2 could be a fully on-chain Anchor program with the same algorithm.

IMPLEMENTATION

Tech Stack

LayerTech
FrontendNext.js 16 (Turbopack), TypeScript, Tailwind v4
Wallet@solana/wallet-adapter-react + Phantom adapter
BackendNext.js API routes (serverless on Vercel)
StateUpstash Redis
ChainSolana (Helius RPC), @solana/web3.js, @solana/spl-token
AuthEd25519 signatures (tweetnacl)
TokenSPL token on pump.fun
HostingVercel
Domainjustdice.fun
SETUP

Deployment

Local development

git clone https://github.com/DICE-SOL/justdice.git
cd justdice
npm install
cp .env.example .env.local
# Fill in env vars (see below)
npm run dev

Required environment variables

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
NEXT_PUBLIC_DICE_MINT=         # SPL token mint after launch
VAULT_PUBKEY=                  # Solana pubkey holding the $DICE
VAULT_SECRET_KEY=              # bs58-encoded private key for the vault
RPC_URL=                       # Helius or other Solana RPC

Generating a vault keypair

solana-keygen new --outfile vault.json --no-bip39-passphrase
solana-keygen pubkey vault.json    # → VAULT_PUBKEY
# bs58-encode vault.json contents → VAULT_SECRET_KEY

The vault must hold $DICE (the initial house pool) and a small amount of SOL for transaction fees.

Production deploy

npx vercel --prod
SOURCES

Verification & Sources

dooglus (original author)BitcoinTalk profile, u=3420
Original announcement threadtopic=238613, 242 pages, June 20, 2013
Original verifier sourcegithub.com/dooglus/just-dice
Our fork of the verifiergithub.com/DICE-SOL/just-dice
OG site (still live)just-dice.com
Wayback (BTC era)Sept 2013 snapshot
Source code (this repo)github.com/DICE-SOL/justdice