SDK & API reference · V2

Integrate in ~10 lines.

Sign-once randomness-as-a-service on KUB. Inherit one Solidity contract and write one function; the frontend SDK + the keeper API settle it for you. Copy-paste Solidity, JavaScript, and the /reveal API below.

Proprietary, not open source. © 2026 Durian (durianfun). Licensed only to integrate with the canonical deployment (see the SDK license). Blockhash commit-reveal, not a VRF — any money consumer must cap the aggregate value per reveal block (§ Money rules).

Quickstart

Inherit EntropyConsumer, request with _roll, and implement _onRandomness. The user signs only play(); a keeper (or anyone) settles it and your callback fires.

MyGame.sol
import "./EntropyConsumer.sol";
import "./EntropyLib.sol";            // optional: many values from one reveal

contract MyGame is EntropyConsumer {
    using EntropyLib for uint256;
    uint32 constant CB_GAS = 150_000;
    mapping(uint256 => address) public player;

    constructor(address entropyV2) EntropyConsumer(entropyV2) {}

    // user signs ONCE here; pass the protocol fee through
    function play() external payable {
        require(msg.value >= entropy.fee(CB_GAS), "fee");
        uint256 id = _roll(keccak256(abi.encodePacked(msg.sender, block.number)), CB_GAS);
        player[id] = msg.sender;
    }

    // fires later when revealed (by a keeper / the next user / anyone)
    function _onRandomness(uint256 id, uint256 rng) internal override {
        uint256 pocket = rng.pick(0, 37);   // 0..36, domain-separated
        // ... settle / pay out / mint to player[id] ...
    }
}

Addresses & config

ItemValue
ChainKUB Chain (Bitkub) · chainId 96 (0x60)
RPChttps://rpc.bitkubchain.io
DurianEntropyV20xE2BACB42Ce5C5C1FB32335ac06B79bE28fb54caB
Keeper APIhttps://durian-entropy-keeper.pupkaikub.workers.dev/reveal
Explorerhttps://www.kubscan.com
Compilersolc 0.8.24 · optimizer 200 · evm paris

V2 is the commercial service (gas-scaled fee, sign-once UX), built on the same blockhash commit–reveal primitive.

Solidity SDK — the base handles the dangerous parts

Inherit EntropyConsumer and you get all of this for free:

_roll(bytes32 seed, uint32 cbGas) → uint256 idinternal

Pays entropy.fee(cbGas), requests a callback, and records the id as yours (known[id]) so a spoofed callback for a foreign id can never trigger your _onRandomness.

onRandomness(uint256 id, uint256 rng)external · gated

Implemented for you: requires msg.sender == entropy AND known[id], single-delivery (delivered[id]), then calls your _onRandomness.

settle(uint256 id)external · recovery

If the auto-callback ever failed (out of gas, transient revert), anyone may poke this once the request is FULFILLED to deliver the stored result exactly once.

_tryReveal(id) · _trySeal(id)internal · self-crank

Settle/capture a previous pending request inside a new action, so users almost never sign a second tx.

You implement exactly one function: _onRandomness(uint256 id, uint256 rng).

Contract functions (DurianEntropyV2)

requestCallback(bytes32 seed, address consumer, uint32 cbGas) → uint256 idpayable

Pay the fee, get a callback to consumer on reveal. (What _roll calls.)

request(bytes32 seed) → uint256 idfree

No callback, no fee — settle it yourself with seal/reveal.

fee(uint32 cbGas) → uint256view

The fee to send at the current gas price: markupBps/1e4 × (baseGas + cbGas) × max(tx.gasprice, minGasPrice).

seal(uint256 id) · sealBatch(uint256[] ids)cheap

Capture blockhash(revealBlock) within the 256-block window so the result can be revealed later with no deadline.

reveal(uint256 id) → rng · revealBatch(uint256[] ids)permissionless · idempotent

Compute the result + fire the callback. Works anytime once sealed; else within the window.

status(uint256 id) → uint8view

0 NONE · 1 WAITING · 2 READY · 3 EXPIRED · 4 FULFILLED.

results(uint256 id) → uint256view

Stored randomness (0 until revealed).

Frontend SDK (JS) — request, then settle with a user fallback

The one rule: never depend solely on the keeper. settle() pings the keeper, watches for the result, and if the keeper is silent it falls back to a user-signed seal+reveal.

play.js (ethers v6)
import { DurianEntropy } from "./durian-entropy.js";

const entropy = new DurianEntropy({
  address: "0xE2BACB42Ce5C5C1FB32335ac06B79bE28fb54caB",
  workerUrl: "https://durian-entropy-keeper.pupkaikub.workers.dev/reveal",
  provider,                                  // ethers read provider
});

// 1) user signs ONE tx — your dapp's play()/mint()
const rc = await (await myGame.connect(signer).play({ value: await entropy.fee(150000) })).wait();
const id = entropy.idFromReceipt(rc);

// 2) keeper-first, user-fallback — always settles within the window
const rng = await entropy.settle(id, {
  signer,
  onState: (s) => setStatus(s),       // 'settling'→('fallback'→'sealing'→'revealing'→)'done'
});
MethodNotes
fee(cbGas)fee (wei) to send for a request
idFromReceipt(rc)parse the Requested event id
settle(id, {signer, onState, timeoutMs, pollMs})the locked pattern — keeper-first, user-fallback. onState also gets {elapsedMs, remainingMs}
userSettle(id, signer, onState)the fallback path alone (seal if needed → reveal)
status · results · sealedHashreads
Typed errors on e.code: EXPIRED · NO_SIGNER · NOT_READY · USER_REJECTED.

Keeper API — POST /reveal

The Durian keeper is a Cloudflare Worker. Your frontend pings it right after the request tx; it settles on-ping as soon as the reveal block is mined (~6–9s). It is gated — it verifies the id on-chain before acting, so it can't be spammed into wasting gas. You normally never call it directly (settle() does).

POST /reveal
POST https://durian-entropy-keeper.pupkaikub.workers.dev/reveal
Content-Type: application/json

{ "id": "<request id as decimal string>" }
StatusBodyMeaning
200{ ok: true, queued }accepted; settling on-ping / next sweep
200{ ok: true, note }already fulfilled
404{ error }id doesn't exist on-chain (rejected, no gas spent)
400 / 503{ error }malformed / keeper not configured

Fees & cbGas

fee(cbGas) ≈ one reveal's gas × markup (currently 2×), tracked to the live gas price with a floor. Read it live and send it with the request. Overpay is safe — refund the remainder in your consumer.

Your callback doesSuggested cbGas
record / read a value (roulette pocket)80k – 120k
coinflip payout via pull ledger120k – 180k
NFT _safeMint + storage180k – 250k

Too low → the on-reveal callback fails (caught), and settle(id) recovers it in one extra tx (no fund loss). Ceiling 8,000,000. The fee funds the keeper + the Durian treasury (the fee recipient is an Ownable2Step owner — the only privileged role; it cannot touch randomness, escrows, or pause).

Seal → claim anytime

The EVM keeps only the last 256 blockhashes, so the entropy must be captured within 256 blocks — a hard limit, not a choice. seal(id) stores blockhash(revealBlock) in one cheap SSTORE inside that window; once sealed, reveal(id) (compute + callback) has no deadline — a winner claims whenever. The keeper's only time-critical job is the trivial, batchable seal.

If nobody seals within 256 blocks the request expires and is forfeit (the requester already escrowed). Extending the window is impossible and would be a re-roll exploit, so it is deliberately not done.

Rules every MONEY consumer MUST follow

1
Escrow at request time. Take the stake in the same tx as _roll. A loser who refuses to settle gains nothing — the stake is already yours; unsettled → expiry forfeits it.
2
Cap the AGGREGATE value per reveal block (not just per bet). A block producer can bias one blockhash. Track committedAtBlock[block.number+1] += stake; require(≤ perBlockCap) and size perBlockCap below KUB block_reward / 10. See examples/CoinFlipHouse.sol.
3
Never let a pre-reveal animation decide value. The only source of truth is the callback / results(id). Show a cosmetic spin, ease to the real result on the event.
4
Pay winners via a pull ledger, never a .call inside the callback (a reverting receiver would strand your state). Credit winnings[player] + add claim().
5
On expiry, forfeit — never refund-and-re-roll. Re-rolling is a grinding exploit.

Lifecycle & errors

lifecycle
request ──(block N+1 mined)──> WAITING ─> READY ──(seal <=256 blk)──> READY (forever) ─> FULFILLED
                                            └─(no seal, 256 blk pass)──> EXPIRED (forfeit)
RevertWhen
too earlybefore the reveal block is mined (also blocks atomic grinding)
expired256-block window passed unsealed
no requestunknown id
feeunderpaid
cbGas too high> 8,000,000

Reference consumers to copy: CoinFlipHouse.sol (money game), RandomTierNFT.sol, RouletteConsumer.sol. Full guide: download the SDK (.md).

↓ SDK.md