What is ShinyLuck.
ShinyLuck is a fully on-chain casino on Somnia where the house is a quorum of autonomous AI agents. Every reel stop, every dice roll, every roulette spin is the keccak256 of three numbers anyone can audit - server seed (committed before bet), client seed (player-supplied), and a future blockhash. Money flow is pull-payment; the casino contract never sends in a callback. House RTP is adjusted hourly by an LLM agent invoked via the Somnia Agent Platform - judges can watch that loop happen on the explorer.
Connect and play in 60 seconds.
- Click "Connect Wallet" in the top-right. Email login via Privy, or any EIP-1193 wallet (MetaMask, Rabby, OKX, Coinbase).
- Switch to Somnia testnet (Shannon). Chain ID
50312. Auto-added by the modal. - Get STT from testnet.somnia.network/faucet.
- Pick a game. Sugar.Lab and Dice are the polished demos. Hit place bet - the receipt opens ~1.5 s later when the reveal-bot lands the seed.
Supported wallets.
Primary path is Privy embedded wallet - email login, no extension. We split the sign/broadcast: Privy signs in-iframe, our SDK broadcasts via our own RPC (~300 ms saved per bet vs Privy's hosted broadcast). Falls back to any EIP-1193 injected provider (MetaMask, Rabby, OKX, Coinbase, WalletConnect). The frontend SDK is in frontend/lib/shinyluck-sdk.js.
Getting STT testnet tokens.
STT is the gas + bet token on Shannon (Somnia testnet). Drip from testnet.somnia.network/faucet; 0.5 STT per request is plenty for hundreds of small bets. On mainnet the symbol becomes SOMI.
Sugar.Lab.
The flagship. 7×7 cluster-pays grid with cascading wins, multiplier accumulator, "Charge Meter" loyalty progression, and Buy Bonus (100× unit stake → high-variance bonus round). RTP 92.00% baseline; jumps to ~97.4% during Bonus Mode. Math is in contracts/lib/ClusterLib.sol; payout boost constant CLUSTER_PAY_BOOST_X100 = 332 (calibrated via 100 k Monte Carlo run in scripts/_rtp-simulation.js).
Dice.
Pick a threshold in [2, 98] and a direction (over/under). Roll = randomness mod 100 + 1. Payout multiplier = (10000 − edge) / (winChance × 100). 1% house edge → 99% RTP, the highest in the lineup.
Crash.
Bustabit-style multiplier curve. You pre-commit an auto-cashout in [1.01×, 100×] and stake. The contract uses keccak256(serverSeed ‖ clientSeed ‖ blockhash ‖ nonce) to derive crashPoint with a 1-in-33 instant-crash baked in. 3% house edge.
Vault.7 (slots).
3 reels × 7 symbols, 15-cell payout grid, 5 paylines + scatter. Bonus rounds via Buy Bonus (75× unit). Loyalty Free Spins issued every 50 paid spins. Baseline RTP 92%; Bonus Mode adds +4.5% (paytable boost in _settleBet bumps from 560 → 592). Math IS the edge - there's no houseEdgeBps applied on top.
Plinko.
16 rows × 17 slots; ball path = 16 bits of randomness, one per peg row. Three risk tiers: low (max 16×), medium (max 110×), high (max 1000×). 1.5% house edge baked into symbol weights.
Mines.
5×5 grid. Pick [1, 24] mines; HouseManager pre-commits the seed before you place, mine positions only lock when seed reveals. Multiplier scales as ∏ (25-i)/(25-mines-i) × (1 − edge); 100× cap. 1.2% house edge.
Roulette.
American double-zero wheel. 10 bet kinds - straight (35:1), red/black/even/odd/1-18/19-36 (1:1), dozens (2:1). Result = randomness mod 38. 5.26% house edge (standard American wheel; matches industry quote).
Bet placement latency.
Total wall-clock from click to settled receipt averages 2.0-2.5 s; best observed 1.93 s. Breakdown (Sugar.Lab spin):
- Populate (parallel RPC for gas + nonce + fees, 5 s TTL cache) - 80-150 ms
- Privy iframe sign (eth_signTransaction, returns raw hex) - 350-500 ms
- Broadcast (eth_sendRawTransaction via our RPC) - 80-150 ms
- 1 block to commitBlock + REVEAL_DELAY=1 (Somnia ~400 ms/block) - 800 ms
- Reveal-bot settle (WebSocket push, leading-edge debounced 150 ms) - 400-600 ms
Pre-sign pipeline (Sugar-only).
While the cascade animation plays (1-3 s), the SDK speculatively pre-signs the next spin in the Privy iframe. On the user's next click only the broadcast round-trip remains (~150 ms). Reservation safety:
- Pre-sign expires after 25 s (fee-data TTL window)
- Stake mismatch between presign + click → drop + re-sign
- Broadcast error → invalidate + force chain-nonce resync (no mempool gap)
- Nonce mutex serialises concurrent presign + auto-claim populate calls
Extending the pattern to Vault.7 / Plinko is one-liner once the animation hook lands; see frontend/lib/shinyluck-sdk.js presignClusterSpin.
Real-time updates.
Every game page subscribes to BetSettled for the connected player via Somnia WebSocket (wss://api.infra.testnet.somnia.network/ws - the canonical infra endpoint used by every first-party Somnia example). The lobby + leaderboard + account aggregate BetSettled across all historical Casino deployments (the redeploy-survivable history list is in frontend/lib/config.js as historicalCasinos). For the explorer/log scans we use the Shannon Explorer REST indexer - one HTTP call per contract per event type vs hundreds of chunked eth_getLogs RPC calls (~100× faster cold start).
Architecture · the agent quorum.
ShinyLuck uses three orthogonal autonomous agent paths, all settled on-chain via Somnia primitives:
-
-
-
-
- HM ↔ LLM hourly loop - autonomous RTP adjustment via Somnia Agent Platform
- Agent Quorum Verifier - 3-of-4 redundant randomness re-derivation per settled bet (defence-in-depth)
- Player Agents - on-chain LLM-driven strategies dispatched by HouseManager every hour, vault-isolated funds, vault-funded LLM ticks + per-user daily/total/per-game limits
HM ↔ JSON API ↔ LLM autonomous chain.
This is the agent-first design story. Every hour Somnia's Reactivity precompile (0x0100) fires a Schedule event → HouseManager._onEvent wakes up → dispatches a 2-stage agent chain via IAgentRequester.createRequest on the Somnia Agent Platform.
Stage 1 - JSON API Agent (id 13174292974160097713, price 0.03 STT × 3 workers) fetches the live ShinyLuck Competitor RTP Research feed and extracts the slots_rtp_avg_bps / cluster_rtp_avg_bps field. Three Somnia validators independently hit the URL, reach Majority consensus on byte-identical output, and the platform calls back with the number.
// Stage 1 payload - what the JSON API Agent receives fetchUint("https://jsonblob.com/api/jsonBlob/019e555c-25db-7e74-a62d-31c135e87539", "slots_rtp_avg_bps", 0) // → returns uint256(9533) → casino RTP avg of 95.33% across competitors
Stage 2 - LLM Inference Agent (id 12847293847561029384, Qwen3-30B with temp=0, price 0.07 STT × 3 workers) receives a prompt built on-chain by _buildDecisionPrompt and replies with one of [LOWER, HOLD, RAISE, BIG_BONUS] via inferString + allowedValues (guaranteed parseable).
// Stage 2 prompt - actual on-chain construction "Decide RTP for VAULT.7 slots. Our RTP: 92.00%. Competitor average RTP (live from research feed): 95.33%. Free bankroll: 50 STT. 1h bankroll change: +0.00%. Action: RAISE = +1.5% RTP. LOWER = -1.5% RTP. HOLD = no change. BIG_BONUS = activate 60min Bonus Mode."
Majority consensus across 3 LLM workers → handleResponse applies via casino.adjustSlotRTP(game, newRtp, "LLM consensus via Somnia Agent Platform"). The whole chain is on-chain - no off-chain script, no admin key, no oracle middleware. Each hour costs ~0.72 STT (2× JSON 0.12 + 2× LLM 0.24); HM holds 48 STT buffer = ~2.5 days unattended runway. Events to watch on Shannon Explorer: CompetitorRtpRequested → CompetitorRtpResolved → RtpAnalysisRequested → RtpAnalysisResolved → casino.RtpAdjusted.
Agent quorum verifier.
AgentQuorumVerifier.sol asks 4 LLM workers to independently re-derive the keccak256(serverSeed ‖ clientSeed ‖ blockhash ‖ nonce) for a settled bet. The signature count (3-of-4 = OK, 2-of-4 = warning, ≤1 = critical) lands in a QuorumResult event. Money never depends on this - randomness is on-chain via blockhash. Quorum is defence-in-depth on the agent layer.
JSON API Agent.
Live-fetches the ShinyLuck Competitor RTP Research feed from a public JSON endpoint (currently jsonblob.com; owner-replaceable via setCompetitorFeedUrl). The feed is manually curated from public casino-review aggregators (askgamblers.com, casinoguru.com, gambling.com) - sample of Stake.us, BC.game, Wink.com slot/cluster RTPs. The agent extracts a JSONPath selector (slots_rtp_avg_bps or cluster_rtp_avg_bps) and 3 validators reach Majority consensus on the integer value.
Parse Website Agent.
HTML → structured data scraper. Reserved for v2 (news-driven Bonus Mode - e.g. "Bitcoin halving today" → activate generous payout window). Not yet bound to the v1 contracts but the agent ID is registered + the platform fee model is wired the same way as the LLM Inference Agent.
Player Agents.
PlayerAgentRegistry.sol + AgentVault.sol. Each player registers once, gets a fresh AgentVault, and authorises HouseManager (and an optional off-chain fallback relayer) to place bets on their behalf within stake / game / daily / total limits. Every hourly Reactivity tick, HouseManager iterates active players and fires a per-player Somnia LLM Inference Agent request (Qwen3-30B, 3-of-3 majority, allowedValues = [SKIP, DICE_0.1, DICE_0.5, SLOTS_0.5, CLUSTER_0.5, PLINKO_0.5, ROULETTE_0.5] filtered by the user's mask). Callback applies the decision via registry.executeBet from the vault. The ~0.24 STT LLM cost is pulled from the player's vault via registry.collectAgentFee right before dispatch - empty vaults are skipped, so spam costs the casino zero. Owner can flip the split via setAgentDecisionSubsidyBps (0 = user pays 100%, 5000 = 50/50, 10000 = casino pays). Supports: Dice, Vault.7, Plinko, Roulette, Sugar.Lab.
Bonus Mode.
HouseManager triggers activateBonusMode(durationMinutes, reasoning) on Casino when 1-hour bankroll growth exceeds +10%. Effects per game:
- Dice / Crash / Mines / Plinko / Roulette - house edge halved (e.g. Dice 1% → 0.5%, Roulette 5.26% → 2.63%)
- Vault.7 (slots) - pay-table boost +32 points (560 → 592 X100), ~+4.5% RTP (92% → 96.5%)
- Sugar.Lab (cluster) - pay-table boost +18 points (332 → 350 X100), ~+5.4% RTP (92% → 97.4%)
The "bps / 2" branch in houseEdgeBps alone was a no-op for the zero-edge slot games (math IS the edge); the additive boost in _settleBet makes Bonus Mode actually meaningful there.
JS SDK.
Embed any ShinyLuck game in a dApp via frontend/lib/shinyluck-sdk.js:
// import the SDK + ethers import { ShinyLuck, CHAINS } from "./lib/shinyluck-sdk.js"; const sl = new ShinyLuck({ casino: "0x01D31a1aD1D82F0409dD1d5a7680065F71a3dbB3", registry: "0x54f686118588Ec935530C84d4752Dd7eAA95E9c9", network: CHAINS.somniaTestnet, }); await sl.connect(); // Sugar.Lab spin (with auto pre-sign of the next one) const { betId, txHash } = await sl.placeCluster("0.1"); const settled = await sl.waitForSettle(betId); if (settled.won) console.log("+", settled.payout); // Pre-sign the next spin during the cascade animation sl.presignClusterSpin("0.1");
Contract addresses.
| Contract | Network | Address |
|---|---|---|
| Casino.sol | Somnia testnet (Shannon) | - |
| HouseManager.sol | Somnia testnet (Shannon) | - |
| PlayerAgentRegistry.sol | Somnia testnet (Shannon) | - |
| AgentQuorumVerifier.sol | Somnia testnet (Shannon) | - |
| SomniaAgentPlatform | Somnia testnet (Shannon) | 0x037B…6776 |
| SomniaReactivityPrecompile | (precompile) | 0x0100 |
All four ShinyLuck contracts are verified on Shannon Explorer - source readable from the address page.
Events & webhooks.
All public state changes are emitted as Solidity events on the Casino + HouseManager contracts:
Casino.BetPlaced(betId, player, game, amount, clientSeed, commitBlock, seedIdx, params)Casino.BetSettled(betId, player, game, won, payout, randomness, serverSeed, clientSeed, blockHash, nonce, resultData)Casino.BetRefunded(betId, player, amount, reason)- emitted when the blockhash window expiresCasino.WithdrawalCredited / WithdrawalClaimed- pull-payment ledgerCasino.BonusModeActivated(until, reasoning)- HM autonomous decisionCasino.RtpAdjusted(game, oldRtpBps, newRtpBps, reasoning)- RTP flex (LLM or rule-based)Casino.ReasoningLog(thought, timestamp)- HM narrates its decisionsHouseManager.ReactiveBetSettledHandled(newMaxBet, freeBankroll)- per-bet reflexHouseManager.ReactiveHourlyTick(freeBankrollNow, changeBps)- cron pulse via precompileHouseManager.RtpAnalysisRequested / RtpAnalysisResolved- LLM agent chainHouseManager.PlayerDecisionRequested(requestId, player, vaultBalance, spentTodayWei, dailyLimitWei)- per-user LLM decision dispatchedHouseManager.PlayerDecisionResolved(requestId, player, decision, game, stakeWei, betId, placed)- the LLM Player Agent (inferToolsChat) finished a tick: it either yielded a placeBet tool call (executed via the vault) or chose to skipHouseManager.PlayerAgentToolCall(requestId, player, game, stakeWei, betId, placed)- the model itself decided the game + stake by calling the on-chain placeBet toolPlayerAgentRegistry.BetExecuted / AgentVault.AgentFeePaid- executor placed a vault-funded bet / vault paid its share of the LLM callAgentQuorumVerifier.QuorumResult(betId, requestId, expected, signers, totalWorkers, level, status, sampleResponse)
For agents · discover & play autonomously.
ShinyLuck is agent-first: a third-party agent can discover the casino's live state and play it with no backend and no API key. Everything starts from one machine-readable manifest:
- /agent-manifest.json - contract addresses, chain config, game ids, bet entrypoints, event schema, and the provably-fair formula. Auto-regenerated on every deploy so the addresses never drift.
Discover (one read). Call HouseManager.agentManifest() - it returns live state in a single view: free bankroll, bonus-mode flag, the open roulette round id + bet-window-end, and per-game reported RTP / max bet / paused. No event scraping needed to decide whether and what to bet.
Play directly. Sign and send the Casino entrypoints yourself: placeDiceBet, placeSlotsBet, placeClusterBet, placeMinesBet, placeplinkoBet, placeCrashBet, placeRouletteBets. You pass a clientSeed; results land in BetSettled and are provably fair (verify the randomness against the revealed serverSeed).
Or run a managed Player Agent. Call PlayerAgentRegistry.registerAgent(string strategy, uint256 dailyLimit, uint256 totalLimit, uint8 allowedGamesMask) with a plain-English strategy (stored verbatim on-chain, max 200 chars), fund the returned AgentVault, and the on-chain House Manager fires an hourly inferToolsChat tick: the Somnia LLM Inference Agent reads your strategy + live state and decides whether to bet by yielding a placeBet(uint8 game, uint96 stakeWei) on-chain tool call, which the House Manager executes from your vault under your limits. The model itself drives the bet - you only pay for your own LLM ticks from the vault.
Provably fair · how the math works.
For every bet: randomness = keccak256(serverSeed ‖ clientSeed ‖ blockhash(commitBlock + REVEAL_DELAY) ‖ nonce). REVEAL_DELAY = 1 block. The server seed's hash is committed before any bet binds to its slot (see provisionSeedHashes); the blockhash is unknowable at commit time and only finalises 1 block later. The browser-side verifier on the Provably Fair page re-derives this exactly and compares against the on-chain value. The Agent Quorum Verifier asks 4 independent LLM workers to do the same - a 3-of-4 majority is the soft confirmation, but the money path doesn't wait on it.
Audit / verification.
All four contracts have source verified on the Shannon Explorer. CommitReveal library matches the formulation above (single file, ≈ 80 lines). RTP claims for slot games are independently checkable: scripts/_rtp-simulation.js runs a 100 k Monte Carlo against ClusterLib / Vault7Lib and prints actual vs claimed. Bug bounty: anything that lets a player or the operator deviate from the published RTPs is in scope; report to security@shinyluck.gg.