Quickstart
A consumer contract needs three things: forward the fee with a request, inherit the consumer base, and implement one callback. Complete working example:
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;
import {IDicedEntropy} from "diced/IDicedEntropy.sol";
import {IDicedConsumer} from "diced/IDicedConsumer.sol";
contract CoinFlip is IDicedConsumer {
IDicedEntropy public immutable entropy;
event FlipResult(uint64 indexed seq, bool heads);
constructor(address entropy_) { entropy = IDicedEntropy(entropy_); }
function flip(bytes32 userRandom) external payable returns (uint64 seq) {
// send at least getFeeV2(); any surplus is credited back to you
seq = entropy.request{value: msg.value}(userRandom, 0);
}
function getEntropy() internal view override returns (address) {
return address(entropy);
}
function entropyCallback(uint64 seq, address, bytes32 randomNumber) internal override {
emit FlipResult(seq, uint256(randomNumber) & 1 == 0);
}
}
Map the 32-byte word onto your outcome space with modulo arithmetic
(uint256(randomNumber) % sides). One word is enough for many sub-outcomes —
slice bits or re-hash with a domain separator.
userRandom client-side with
crypto.getRandomValues per request. It's your half of the entropy: the provider
committed its chain before your request, and you commit your contribution before the reveal.
Without a contract
You can request from an ordinary account, which is what the
demo on the home page does. Send requestV2() to the oracle
with getFeeV2() attached, then read the word out of the Revealed
event rather than a callback — an EOA has no code to call back into, so the oracle's delivery
call is a harmless no-op and callbackSucceeded comes back true.
// 1. fee, 2. request, 3. read the word off the event
const fee = await entropy.read.getFeeV2();
const hash = await entropy.write.requestV2({ value: fee });
const receipt = await client.waitForTransactionReceipt({ hash });
// sequenceNumber is topic 2 of Requested; watch Revealed for the same one
const logs = await client.getLogs({
address: ORACLE, fromBlock: receipt.blockNumber,
event: parseAbiItem('event Revealed(address indexed provider, uint64 indexed sequenceNumber, address indexed requester, bytes32 randomNumber, bool callbackSucceeded)'),
args: { sequenceNumber: seq },
});
const word = logs[0].args.randomNumber;
keccak256(reveal, yourContribution, provider, seq, you) and still checked against
the provider's commitment on-chain before it is emitted. But requestV2() with no
arguments derives your contribution automatically — pass your own via
requestV2(provider, userRandomNumber, gasLimit) if you want to prove you chose it.
Anything settling real value should use a consumer contract so the result arrives atomically.
Addresses
| What | Value |
|---|---|
| Oracle — mainnet | 0x09aB0B2c0fAC9B3a8F28DEA45C71030F3c8A3B32DicedEntropyV2 · source-verified · chain 4663 |
| Oracle — testnet | 0x9781059578EA4578952839a1ae6e6Cefe963D796same source, own keeper · chain 46630 · faucet |
| Agent endpoint | POST https://x402.diced.fun/x402/v1/randomHTTP 402, $0.05 USDG per word |
| RPC | https://rpc.mainnet.chain.robinhood.comtestnet: https://rpc.testnet.chain.robinhood.com |
| Explorer | robinhoodchain.blockscout.com testnet: explorer.testnet.chain.robinhood.com |
| $DICED token | 0x75115dFa63bbC1c1812F9c2ad8bC5ea77519D524official token · ERC-20, 18 decimals · chain 4663 (mainnet only) |
| Legacy oracle (V1) | 0xc934f1c9e2A9CDdc19Ed2E4a1F88DC8d24EA48eEstill served, for consumers that hardcoded it — don't build on it |
API reference
Requesting
| Function | Notes |
|---|---|
request(bytes32 userRandomNumber, uint32 callbackGasLimit) payable → uint64 |
V1-era entry point, kept for compatibility. On V2 prefer requestV2: it takes at least the fee and credits any surplus back to you, so a fee change cannot strand a request in flight. Gas limit 0 = provider default (200k), capped by the on-chain max (2M). Returns your sequence number. |
requestV2() payable → uint64requestV2(uint32) payable → uint64 |
Convenience overloads with an auto-derived user contribution (a salt — fine when you just need "unpredictable to everyone but the provider", weaker than supplying your own). |
requestWithToken(address provider, address token, bytes32 userRandomNumber, uint32 callbackGasLimit) → uint64 |
Pay the fee in an enabled ERC-20 (WETH at launch). Approve first; exact amount pulled, fee-on-transfer tokens rejected. The leading provider is a V2 addition — V1's three-argument form does not exist here. |
Reading
| Function | Notes |
|---|---|
getFee() → uint128 / getFee(address token) → uint96 | Current fee. Always read at call time — don't hardcode. |
failedCallbackRandomness(address provider, uint64) → bytes32 | Your word, if your callback reverted (see below). Zero otherwise. Keyed by provider on V2 — the one-argument V1 form does not exist here. |
getRequestV2(address provider, uint64) → Request | Pending request struct; zeroed once revealed or refunded. |
Refunds
refundRequest(uint64 seq) — callable by anyone once a request has been
pending longer than the refund delay (10 minutes at launch). The fee returns to the original
requester in the original token, always. Native-fee consumers must be able to receive ETH
(have a receive()), or use WETH.
Events
event Requested(uint64 indexed sequenceNumber, address indexed requester,
address feeToken, uint96 feePaid, uint32 callbackGasLimit, bytes32 userContribution);
event Revealed(uint64 indexed sequenceNumber, address indexed requester,
bytes32 randomNumber, bool callbackSucceeded);
event CallbackFailed(uint64 indexed sequenceNumber, address indexed requester,
bytes32 randomNumber, bytes reason);
event Refunded(uint64 indexed sequenceNumber, address indexed requester,
address feeToken, uint96 amount);
The callback
- Only the oracle can invoke
_entropyCallback; theIDicedConsumerbase enforces it. You override the internalentropyCallback. - It runs under your requested gas limit. Keep it lean; do heavy work in a later user transaction.
- A revert in your callback does not undo the reveal. The word is stored in
failedCallbackRandomness(provider, seq)and aCallbackFailedevent fires — build a self-serve path if your logic can revert. - Reveals can arrive in any order relative to other requests. Key your state by sequence number, never by "latest".
Fees & refunds lifecycle
- Request: fee escrows inside the oracle, tied to your request.
- Reveal: escrow becomes operator revenue — only now.
- No reveal: after the delay, anyone triggers your refund; escrow returns to you in full.
The operator cannot withdraw escrow of pending requests — the accounting makes it impossible, not just impolite.
TypeScript
import { getFee, requestRandomness, waitForCallback } from '@diced/sdk';
const fee = await getFee(publicClient, ENTROPY);
const { sequenceNumber } = await requestRandomness(publicClient, walletClient, ENTROPY);
const { randomNumber } = await waitForCallback(publicClient, ENTROPY, sequenceNumber);
The SDK ships viem-based helpers plus the Solidity interfaces. Contact us for access while the repository is private.
Migrating from other entropy oracles
The consumer surface is deliberately Pyth-Entropy-shaped: requestV2(), the
_entropyCallback(uint64, address, bytes32) selector, fee-forwarding semantics.
If your contract already consumes a Pyth-style entropy service, migration is: point it at the
diced.fun oracle address. The only behavioral differences to review are the flat (not
gas-scaled) fee, the exact-msg.value rule, and the refund flow above.
Trust model
What is cryptographically guaranteed, and what you still take on trust. No hand-waving.
Guaranteed
- No grinding. The provider's hash chain is committed on-chain before your request; your contribution lands before the reveal. The word is
keccak256(providerReveal, yourContribution, seq, you)— neither side alone chooses it. - Verifiability. Every reveal is keccak-verified on-chain against the commitment; anyone can recompute every delivered word from public data.
- Isolation. Other consumers' reverting or gas-hungry callbacks cannot affect your delivery.
- Refundability. An unrevealed request is always refundable, permissionlessly. Funds cannot be stuck.
Taken on trust
- Withholding. The provider knows future reveals and could decline to publish one, converting an outcome into a refund. It cannot substitute a different outcome. Design your application so refund-instead-of-delivery is an acceptable worst case.
- Provider participation. Knowing its future reveals, the provider could act as a user of your downstream application with a chosen contribution. Relevant only if you don't trust the operator; visible on-chain if it happens; not cryptographically prevented in any single-provider design.
- Liveness. One keeper serves reveals (1–3s typical). Downtime means delayed delivery, then refunds — never stuck funds.