$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.
| Provably fair | HMAC-SHA512 with rejection sampling. Verifiable using dooglus's open-source verifier. |
| Zero gameplay sell pressure | Losses recycle to the house pool. Tokens are never market-sold from gameplay. |
| Deflationary | 1% of every bet is burned permanently. |
| Off-chain ledger | Bets are instant Redis updates. Deposits/withdrawals settle on Solana. |
| Custodial vault | Same trust model as OG Just-Dice (held 50,000 BTC). On-chain proof of solvency. |
| Wallet sig auth | Every game action requires an Ed25519 signature. |
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.
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.
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.
Game Mechanics
Placing a bet
A bet is parameterized by:
target— a number from 0.01 to 99.98rollHigh— boolean. Iftrue, you win whenroll > target. Iffalse, whenroll < target.betAmount— your wager in$DICEraw 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 = true → chance = 50.49%, multiplier = 1.9608×. A 100 $DICE bet has a potential payout of 196.08 (profit 96.08) with ~50% probability.
Settlement
| Outcome | Effect |
|---|---|
| Win | balance += payout, housePool -= profit, burned += betAmount × 1% |
| Lose | balance -= betAmount, housePool += betAmount × 99%, burned += betAmount × 1% |
Limits
| Min bet | 0.001 $DICE (1,000 raw) |
| Min withdraw | 0.01 $DICE (10,000 raw) |
| Max profit | 0.5% of housePool (dynamic) |
| Min chance | 0.01% |
| Max chance | 99.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.
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.
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
- Before any bet, the client fetches
/api/balancewhich returnsseedHash = SHA256(server_seed). The actualserver_seedis never revealed. - Player bets. Each bet uses an incrementing
nonce. Theclient_seeddefaults to"default"but can be set to anything. - Player can rotate seeds at any time via
/api/seeds. This reveals the previousserver_seed, generates a new one, and returns the newseedHash. - 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:
- dooglus.github.io/just-dice — official, by dooglus
- RGBkey verifier
- github.com/DICE-SOL/just-dice — our fork of the verifier
Tokenomics & Flywheel
Value capture
| Force | Direction | Source |
|---|---|---|
| Buy pressure | ↑ Price | Players must buy $DICE to play |
| Burn | ↓ Supply | 1% of every bet, permanently destroyed |
| Loss recycling | Neutral | Lost $DICE → house pool, never sold |
| Creator fees | → SOL Jackpot | Funds bonus prize pool, grows with volume |
The economic loop
- Player wants to play → must buy
$DICE→ buy pressure - Player bets → 1% burned → deflationary
- Player wins → gets
$DICEfrom house pool (no chart impact) - Player loses →
$DICEgoes to house pool (no chart impact) - Trading volume generates pump.fun creator fees → accumulates as SOL jackpot
- Jackpot grows → attracts more players → loop repeats
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 Model
| Threat | Mitigation |
|---|---|
| Bet as another wallet | Ed25519 signature auth — every game request requires a session token tied to a signed nonce. |
| Replay old auth | 5-minute nonce TTL — auth challenges expire if not signed. |
| Race condition on bets | Redis distributed lock (jd:lock:bet:<wallet>, 10s TTL). Concurrent bets return HTTP 429. |
| Race condition on withdrawals | Same lock pattern. Failed transactions trigger atomic balance restore. |
| Double-credit deposits | Transaction signatures stored with 30-day TTL. Already-credited txs return HTTP 409. |
| Vault drain by API bug | Withdrawal debits balance before sending tx. If tx fails, balance is restored. |
| Rigged rolls | Provably fair — pre-committed server seed hash + user-chosen client seed + open-source verifier. |
| Server seed manipulation | Server 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.
Tech Stack
| Layer | Tech |
|---|---|
| Frontend | Next.js 16 (Turbopack), TypeScript, Tailwind v4 |
| Wallet | @solana/wallet-adapter-react + Phantom adapter |
| Backend | Next.js API routes (serverless on Vercel) |
| State | Upstash Redis |
| Chain | Solana (Helius RPC), @solana/web3.js, @solana/spl-token |
| Auth | Ed25519 signatures (tweetnacl) |
| Token | SPL token on pump.fun |
| Hosting | Vercel |
| Domain | justdice.fun |
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
Verification & Sources
| dooglus (original author) | BitcoinTalk profile, u=3420 |
| Original announcement thread | topic=238613, 242 pages, June 20, 2013 |
| Original verifier source | github.com/dooglus/just-dice |
| Our fork of the verifier | github.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 |