SDK & API reference · V2.1

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 EntropyConsumerV21, request with _roll, and implement _onRandomness. The user signs only play(); a keeper (or anyone) settles it and your callback fires.

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

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

    constructor(address entropyV2) EntropyConsumerV21(entropyV2) {}

    // user signs ONCE. Fund this tx from the frontend with entropy.feeAndGas() (over-funds gas = no "fee" revert)
    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
DurianEntropyV210xa32236611eC11980B1F15760eB3432f7e25EFdC8
Keeper APIhttps://keeper-v21.durianfun.xyz/reveal
Explorerhttps://www.kubscan.com
SDK filesdurian-entropy.js · EntropyConsumerV21.sol · IEntropyConsumer.sol · EntropyLib.sol · examples: CoinFlip / NFT / Roulette
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 EntropyConsumerV21 and you get all of this for free:

!
Money / NFT consumers MUST inherit EntropyConsumerV21 — never implement IEntropyConsumer raw. Anyone can point requestCallback(seed, <yourConsumer>, cb) at your contract; the base's known[id] + delivered[id] guards reject spoofed / foreign callbacks.
_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 (DurianEntropyV21)

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

Total fee = bounty + protocol, at the current gas price. Fund a request with the SDK's feeAndGas(), not a bare read (see Fees).

bounty(uint32 cbGas) → uint256view

(bountyGas + cbGas) × max(tx.gasprice, minGasPrice) — reimburses whoever reveals (the keeper). Gas pass-through, not revenue.

protocol(uint32 cbGas) → uint256view

bounty × protocolBps / 10000 — Durian's protocol revenue, booked to the owner at request time.

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: "0xa32236611eC11980B1F15760eB3432f7e25EFdC8",
  workerUrl: "https://keeper-v21.durianfun.xyz/reveal",
  provider,                                  // ethers read provider
});

// 1) user signs ONE tx. Use feeAndGas() (NOT fee()): it OVER-funds (2× gas) + does NOT pin gasPrice,
//    so a wallet gas-bump can't revert "fee". The contract refunds the surplus (your consumer refunds
//    it on the consumer path; reclaim() on the direct path). Never send the exact fee().
const rc = await (await myGame.connect(signer).play({ ...(await entropy.feeAndGas(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) at the current gas price — display only
feeAndGas(cbGas)fund a request with this{value}; spread onto your tx ({...await entropy.feeAndGas(cb)}). OVER-funds (2× gas, no pinned gasPrice) so a wallet gas-bump can't revert "fee"; the contract refunds the surplus (reclaim() on the direct path / your consumer refunds it). Never send the exact fee().
withdrawable(addr) · reclaim(signer)read / withdraw any surplus the contract holds for you
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}. Resolves the RAW rng — with a consumer, read win/lose from YOUR contract's event/state, not this number
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://keeper-v21.durianfun.xyz/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) = bounty + protocol. bounty = (bountyGas + cbGas) × gasprice reimburses whoever reveals (a self-funding keeper market); protocol = bounty × protocolBps/10000 (currently 100% ⇒ fee = 2× bounty) is Durian revenue. Tracked to the live gas price with a floor. Fund a request with entropy.feeAndGas(cbGas) — never send the exact fee(). The fee is computed from tx.gasprice at execution and wallets override the gas price you set (seen live: Rabby bumps 25→30 gwei), so an exact fee reverts "fee". feeAndGas over-funds at 2× gas and does not pin gasPrice; the contract charges only the real fee and refunds the surplus — reclaim() on the direct path, or your consumer refunds the overpay (RouletteHouse does).

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/CoinFlipHouseV21.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: CoinFlipHouseV21.sol (money game), RandomTierNFTV21.sol, RouletteConsumerV21.sol. Full guide: download the SDK (.md).

↓ SDK.md