@d20dao/vrf-sdk 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -1,22 +1,26 @@
1
1
  # d20dao consumer-agent guide
2
2
 
3
- Use this guide when integrating @d20dao/vrf-sdk into an application or interpreting its public evidence. Install with `npm install @d20dao/vrf-sdk` (0.3.4 or newer). The package provides a general randomness interface; dice and mining contracts are examples. Read installed declarations for exact types and match the packaged PROTOCOL-PROVENANCE.json to the deployment being used. The installed API.md (`@d20dao/vrf-sdk/API.md`, generated from the packaged ABIs) lists every D20VRFCoordinator and EpochEntropy function, event and error with its selector, caller, the calls that raise each error and what to do about it.
3
+ Use this guide when integrating @d20dao/vrf-sdk into an application or interpreting its public evidence. Install with `npm install @d20dao/vrf-sdk` (0.4.0 or newer). The package provides a general randomness interface; the dice, raffle and loot-drop contracts under examples/ are the ones to copy. Read installed declarations for exact types and match the packaged PROTOCOL-PROVENANCE.json to the deployment being used. The installed API.md (`@d20dao/vrf-sdk/API.md`, generated from the packaged ABIs) lists every D20VRFCoordinator and EpochEntropy function, event and error with its selector, caller, the calls that raise each error and what to do about it.
4
4
 
5
- Integration order: network parameters and coordinator proxy; install and compiler setup; consumer contract (recommended payment pattern, callback gas); off-chain quote and request through the consumer; wait for and read the result; expiry, refund and retry gas. https://github.com/d20dao/randomizer-demo is a complete dapp built this way (consumer with one function per randomness option, single-page UI, read-only RPC relay), running at https://mainnet-demo.d20dao.org.
5
+ Integration order: network parameters and coordinator proxy; install and compiler setup; consumer contract copied from an example (payment pattern, callback gas); off-chain quote and request through the consumer; wait for and read the result; expiry, refund and retry gas. https://github.com/d20dao/randomizer-demo is a complete dapp built this way (consumer with one function per randomness option, single-page UI, read-only RPC relay), running at https://mainnet-demo.d20dao.org.
6
+
7
+ ## Best practices
8
+
9
+ Quote with quoteFeeAt(callbackGasLimit, the latest header baseFeePerGas) plus a buffer, never with quoteFee through eth_call, where the base fee reads as 0 and the quote collapses to minFee. Send requests from a contract, never an EOA. Keep the callback small: store the result and do the work later; a failed callback is rolled back and anyone can redeliver the same accepted word with retryCallback, with more gas if needed. Map each request id to your own context and reject unknown or already-finished callbacks. Derive many values from one word (diceRoll count, chooseMany, shuffle, or keccak256(abi.encode(word, i))) rather than sending several requests. Handle the 60-second expiry path and choose a refund address that can receive a native transfer or call withdrawRefundCredit. Never re-roll a result you dislike; once fulfilled is true the word is final. Never use blockhash or block.timestamp as randomness. Withdraw the refund credit a fee buffer leaves behind, or return the change in the requesting transaction. Freeze any list before requesting an index into it.
6
10
 
7
11
  ## Networks and toolchain
8
12
 
9
- Arc Mainnet: chain 5042, live service, RPC https://rpc.mainnet.arc.io, explorer https://explorer.arc.io, D20DAO explorer https://arc.d20dao.org, manifest https://d20dao.org/deployments/arc-mainnet.json. Arc Testnet: chain 5042002, development, RPC https://rpc.testnet.arc.io, explorer https://testnet.arcscan.app, D20DAO explorer https://arc-testnet.d20dao.org, manifest https://d20dao.org/deployments/arc-testnet.json. Native USDC (18 decimals) pays gas and request fees; test USDC comes from the faucet linked at https://docs.arc.io/arc/references/connect-to-arc. Use Node 22.13+, solc 0.8.28 and evmVersion cancun. Hardhat resolves `@d20dao/vrf-sdk/contracts/...` from node_modules; Foundry needs the remapping `@d20dao/vrf-sdk/=node_modules/@d20dao/vrf-sdk/`. Consumer sources need no OpenZeppelin. To add a network to a browser wallet, call wallet_addEthereumChain with chainId '0x13b2' (5042, chainName 'Arc Mainnet') or '0x4cef52' (5042002, 'Arc Testnet'), nativeCurrency { name: 'USDC', symbol: 'USDC', decimals: 18 }, and the rpcUrls and blockExplorerUrls above. Website guides: https://d20dao.org/docs, index https://d20dao.org/llms.txt. Public protocol source: the SDK repository's protocol/ folder (https://github.com/d20dao/d20-sdk/tree/main/protocol); the keeper repository named in PROTOCOL-PROVENANCE.json is not public.
13
+ Arc Mainnet: chain 5042, live service, RPC https://rpc.mainnet.arc.io, explorer https://explorer.arc.io, D20DAO explorer https://arc.d20dao.org, manifest https://d20dao.org/deployments/arc-mainnet.json. Arc Testnet: chain 5042002, development, RPC https://rpc.testnet.arc.io, explorer https://testnet.arcscan.app, D20DAO explorer https://arc-testnet.d20dao.org, manifest https://d20dao.org/deployments/arc-testnet.json. Both networks run the upgraded coordinator and registry implementations this package describes, behind unchanged proxy addresses; take addresses and code hashes from the manifests. Native USDC (18 decimals) pays gas and request fees; test USDC comes from the faucet linked at https://docs.arc.io/arc/references/connect-to-arc. Use Node 22.13+, solc 0.8.28 and evmVersion cancun. Hardhat resolves `@d20dao/vrf-sdk/contracts/...` from node_modules; Foundry needs the remapping `@d20dao/vrf-sdk/=node_modules/@d20dao/vrf-sdk/`. Consumer sources need no OpenZeppelin. To add a network to a browser wallet, call wallet_addEthereumChain with chainId '0x13b2' (5042, chainName 'Arc Mainnet') or '0x4cef52' (5042002, 'Arc Testnet'), nativeCurrency { name: 'USDC', symbol: 'USDC', decimals: 18 }, and the rpcUrls and blockExplorerUrls above. Website guides: https://d20dao.org/docs, index https://d20dao.org/llms.txt. Public protocol source: the SDK repository's protocol/ folder (https://github.com/d20dao/d20-sdk/tree/main/protocol); the keeper repository named in PROTOCOL-PROVENANCE.json is not public.
10
14
 
11
15
  ## Public interfaces
12
16
 
13
- Import builtins, mapRandomness, decodeEvidencePacket, replayCoordinator and quoteRequestFee from @d20dao/vrf-sdk. Epoch helpers and MAX_ATTESTATION_AGE also have an /epoch entrypoint. Import coordinatorAbi and epochEntropyAbi from /abi. Solidity consumers use D20VRFConsumer, ID20VRF, D20VRFRequests and RandomnessMapping under /contracts with compiler 0.8.28. ID20VRF exposes quoteFee(callbackGasLimit), quoteFeeAt(callbackGasLimit, baseFee), requestRandomness(clientSeed, callbackGasLimit, refundAddress), requestMappedRandomness(..., spec) and getMappedResult(requestId).
17
+ Import builtins, mapRandomness, decodeEvidencePacket, replayCoordinator and quoteRequestFee from @d20dao/vrf-sdk. The epoch helpers are also exported from the /epoch entry point, which alone exports MAX_ATTESTATION_AGE. Import coordinatorAbi and epochEntropyAbi from /abi. Solidity consumers use D20VRFConsumer, ID20VRF, D20VRFRequests and RandomnessMapping under /contracts with compiler 0.8.28. ID20VRF exposes quoteFee(callbackGasLimit), quoteFeeAt(callbackGasLimit, baseFee), requestRandomness(clientSeed, callbackGasLimit, refundAddress), requestMappedRandomness(..., spec) and getMappedResult(requestId).
14
18
 
15
19
  The package is ESM only, for Node and bundlers; bundle it (Vite, webpack, esbuild) for browsers. quoteRequestFee expects an ethers v6 provider (or getBlock/call with ethers-v6 shapes); with other clients read the latest header baseFeePerGas, add a buffer and call quoteFeeAt. Never quote with quoteFee through eth_call, which reports a base fee of 0.
16
20
 
17
21
  Names that are easy to confuse: refundBps() is the current refund ratio copied into each new request, requestRefundBps(requestId) the ratio one request copied and is refunded at; pricing() and quoteFee price future requests, requestFeePaid(requestId) is what one request escrowed; keeperFeeBps() has no per-request copy and is read at proof acceptance. Solidity RandomnessMapping.Spec and the TypeScript MappingSpec returned by builtins carry the same fields (operation, lower, upper, count, population; lower and upper are bigint in TypeScript), ethers encodes a MappingSpec for the struct unchanged and hashMapping(spec) equals the request's mappingHash.
18
22
 
19
- Frontends: coordinator reverts are custom errors, all in coordinatorAbi, and pass through a consumer's ordinary Solidity call unchanged, so decode revert data with coordinator.interface.parseError(data) (ethers) or decodeErrorResult({ abi: coordinatorAbi, data }) (viem), or add the coordinator's error entries to the consumer ABI. Common responses: IncorrectFee re-quote and resend; InvalidCallbackGas use 30,000–1,000,000; ContractConsumerRequired send through a deployed consumer, not an EOA or constructor; NotFulfilled keep waiting or, after the deadline, refund; RefundNotAvailable not yet past the deadline, already fulfilled or already refunded; InsufficientCallbackGas raise the transaction gas limit. OnlyCoordinator and InvalidCoordinator are in the consumer ABI only. ethers v6 returns structs as Result arrays: a field named like an Array or Result member (values, length, map, filter, keys) is shadowed; use result.getValue(name), position or result.toObject(). Observed 2026-09-17, not guaranteed: all public Arc RPC endpoints are on *.arc.io, which common browser ad-block lists block (net::ERR_BLOCKED_BY_CLIENT), so read through the connected wallet's EIP-1193 provider after checking its chain ID, or through a same-origin read-only JSON-RPC relay such as the randomizer-demo worker/index.js; rpc.mainnet.arc.io rate-limited batched calls from shared Cloudflare egress addresses while rpc.blockdaemon.mainnet.arc.io accepted them, and the rpc.drpc.*.arc.io free plan rejected batches above 3 calls. ethers JsonRpcProvider batches up to 100 calls by default; set batchMaxCount (for example new JsonRpcProvider(url, 5042, { staticNetwork: true, batchMaxCount: 1 })). Cache final values: once fulfilled, randomness and the mapped result never change.
23
+ Frontends: coordinator reverts are custom errors, all in coordinatorAbi, and pass through a consumer's ordinary Solidity call unchanged, so decode revert data with coordinator.interface.parseError(data) (ethers) or decodeErrorResult({ abi: coordinatorAbi, data }) (viem), or add the coordinator's error entries to the consumer ABI. Common responses: IncorrectFee re-quote and resend; InvalidCallbackGas use 30,000–1,000,000; ContractConsumerRequired send through a deployed consumer, not an EOA or constructor; NotFulfilled keep waiting or, after the deadline, refund; RefundNotAvailable not yet past the deadline, already fulfilled or already refunded; InsufficientCallbackGas raise the transaction gas limit. OnlyCoordinator and InvalidCoordinator are in the consumer ABI only. ethers v6 returns structs as Result arrays: a field named like an Array or Result member (values, length, map, filter, keys) is shadowed; use result.getValue(name), position or result.toObject(). All public Arc RPC endpoints are on *.arc.io, which common browser ad-block lists block (net::ERR_BLOCKED_BY_CLIENT), so read through the connected wallet's EIP-1193 provider after checking its chain ID, or through a read-only relay served from your own origin. Some endpoints reject or rate-limit large JSON-RPC batches; ethers JsonRpcProvider batches up to 100 calls by default, so set batchMaxCount (for example new JsonRpcProvider(url, 5042, { staticNetwork: true, batchMaxCount: 1 })). Cache final values: once fulfilled, randomness and the mapped result never change.
20
24
 
21
25
  RequestContext binds chainId, effective coordinator proxy, keyHash, requestId, consumer, clientSeed, mapping, requestBlock, targetBlock, blockHash, epochId and epochHash. Consult Parameters<typeof replayCoordinator>[0] for the complete trusted replay input. configuration.feeRecipient uses the initialized initialFeeRecipient and configuration.initialMinFee the initialize fee argument (getter initialMinFee), not the live payout address or pricing.
22
26
 
@@ -40,9 +44,9 @@ fee = max(minFee, feeMultiplier × baseFee × (fulfillGasOverhead + callbackGasL
40
44
 
41
45
  Send msg.value >= fee. Less reverts with IncorrectFee(expected, actual). Exactly the quote is escrowed (requestFeePaid, emitted as feePaid in RandomnessRequested); any excess is credited to the refund address as refund credit (FeeOverpaymentCredited, refundCredits) and only that address can pull it with withdrawRefundCredit(recipient). Choose a refund address that can call withdrawRefundCredit or receive a plain native transfer.
42
46
 
43
- A contract that requests in the same transaction pays quoteFee(callbackGasLimit), which is exact; D20VRFRequests helpers and MiningRandomnessConsumer do this from the contract balance. A wallet or backend must pay through a consumer contract (requests from EOAs revert) and must never quote quoteFee through eth_call: the base fee is commonly reported as 0 there (verified on Arc mainnet), the quote collapses to minFee and the transaction reverts. Quote with quoteFeeAt(callbackGasLimit, latestBlock.baseFeePerGas) plus a buffer and forward the whole amount. quoteRequestFee(provider, coordinator, callbackGasLimit, { bufferBps = 3000 }) does this with ethers 6: fee is the quote at the block's base fee, value is the quote at a base fee bufferBps higher (equal to fee when the minimum dominates); send value. The buffer covers base-fee movement until inclusion; the excess is refund credit, never revenue. On IncorrectFee, quote again and resend.
47
+ A contract that requests in the same transaction pays quoteFee(callbackGasLimit), which is exact; the D20VRFRequests helpers do this from the contract balance. A wallet or backend must pay through a consumer contract (requests from EOAs revert) and must never quote quoteFee through eth_call: the base fee is commonly reported as 0 there (verified on Arc mainnet), the quote collapses to minFee and the transaction reverts. Quote with quoteFeeAt(callbackGasLimit, latestBlock.baseFeePerGas) plus a buffer and forward the whole amount. quoteRequestFee(provider, coordinator, callbackGasLimit, { bufferBps = 3000 }) does this with ethers 6: fee is the quote at the block's base fee, value is the quote at a base fee bufferBps higher (equal to fee when the minimum dominates); send value. The buffer covers base-fee movement until inclusion; the excess is refund credit, never revenue. On IncorrectFee, quote again and resend.
44
48
 
45
- Recommended consumer pattern: in the requesting function read fee = quoteFee(callbackGasLimit), require msg.value >= fee, pay exactly fee, return msg.value - fee to the caller and use the paying user as refund address. Forwarding msg.value instead (examples/DiceConsumer.sol) leaves the buffer as refund credit that the refund address must withdraw separately; paying from the contract balance needs the application's own funding policy.
49
+ Recommended consumer pattern (examples/DiceConsumer.sol): in the requesting function read fee = quoteFee(callbackGasLimit), require msg.value >= fee, pay exactly fee, return msg.value - fee to the caller and use the paying user as refund address. Forwarding msg.value instead (examples/RaffleConsumer.sol, examples/LootDropConsumer.sol) is shorter but leaves the buffer as refund credit the refund address must withdraw separately; paying from the contract balance needs the application's own funding policy.
46
50
 
47
51
  clientSeed need not be unique or secret: the VRF seed also binds chain, coordinator, key hash, the incrementing request ID, consumer, mapping, request/target block, target hash and epoch. Use it to bind application context, for example keccak256(abi.encode(msg.sender, operationId)) or an item-list commitment.
48
52
 
@@ -54,7 +58,7 @@ A request escrows its quoted fee even if its epoch is unpublished and fixes its
54
58
 
55
59
  D20VRFConsumer authenticates the coordinator proxy; verify the expected request and store the raw callback word with minimal work. callbackGasLimit is 30,000–1,000,000 (InvalidCallbackGas otherwise) and is forwarded exactly to rawFulfillRandomness; the fee grows with it. A new storage slot costs about 22,100 gas; a callback storing the word, fulfillment block and time and two index entries measured about 51,000 gas once its slots were in use and about 105,000 gas for its first result. Measure with eth_estimateGas or a local test and add a margin; the examples use 100,000. Mapped requests still callback with bytes32; use getMappedResult or canonical mapping. Keep application actions and payments separate from the callback.
56
60
 
57
- Valid onchain acceptance at or before requestedAt+60 seconds is timely. A pending transaction is not acceptance. Single requests are normally fulfilled within a few seconds on Arc; in a stress test 200 simultaneous requests were delivered within 36 seconds (median 19 seconds). Timings are not an SLA. If the keeper publishes no epoch packet or proof by the deadline, for any reason, the request expires: nothing is fulfilled late, the application treats it as expired and anyone can refund it. At acceptance keeperFeeBps of feePaid goes to the configured registry committer, not the proof submitter (a failed transfer becomes keeper credit), and the remainder becomes protocol fees. Callback failure still earns the fee; retryCallback redelivers only the same accepted result and cannot pay a second share.
61
+ Valid onchain acceptance at or before requestedAt+60 seconds is timely. A pending transaction is not acceptance. Single requests are normally fulfilled within a few seconds on Arc; in a stress test 200 simultaneous requests were delivered within 36 seconds (median 19 seconds). Timings are not an SLA. If the keeper publishes no epoch packet or proof by the deadline, for any reason, the request expires: nothing is fulfilled late, the application treats it as expired and anyone can refund it. At acceptance keeperFeeBps of feePaid goes to the proof submitter when the registry authorizes it (isAuthorizedCommitter: committer() or an allowed backup committer) and to committer() for any other submitter (a failed transfer becomes keeper credit); the remainder becomes protocol fees. Callback failure still earns the fee; retryCallback redelivers only the same accepted result and cannot pay a second share.
58
62
 
59
63
  Read results from coordinator events (RandomnessRequested carries requestId; RandomnessFulfilled, CallbackAttempted, RequestRefundedTo, RefundCallbackAttempted) or poll getRequest(requestId): fields consumer, callbackGasLimit, requestBlock, targetBlock (0 until publication), deadline, refundAddress, clientSeed, mappingHash, blockHash, randomness (zero until fulfilled), proofHash, transcriptHash, fulfilled, delivered, refunded, epochId (fixed at request time), epochHash (zero until publication). Take requestId from the coordinator's RandomnessRequested log in the request receipt (FeeOverpaymentCredited precedes it when there is excess). When polling, read the latest block first and then getRequest; fulfilled means final, and not fulfilled with block timestamp > deadline means expired. getMapping(requestId) returns the stored Spec; getMappedResult(requestId) returns uint256[] and reverts NotFulfilled before acceptance; mapRandomness(randomness, spec) is a pure mapping of any word. Only getMappedResult is in ID20VRF; the rest are in coordinatorAbi. A request unfulfilled after its deadline can only be refunded.
60
64
 
@@ -66,28 +70,34 @@ Keepers may fulfill up to 16 requests in one fulfillRandomnessBatch transaction.
66
70
 
67
71
  ## Verification and trust
68
72
 
69
- The four ordered recipe slots are Hyperliquid BTC volume, ANU, TickerLayer BTCUSD and TickerLayer ETHUSD; the latter two share a provider signer. The epoch anchor selects one slot; fallback attempt n (1–3) is the slot n positions later, valid only from n × 20 blocks into the epoch (getEpochFallbackSelection, fallbackOpensAt, commitEpochFallback). replayEpochCommitment derives the attempt from the committed source and rejects a commit block before its window. Preserve the entire exact signed response, limited to 128 bytes. Signatures establish wrapper provenance, not unbiased upstream data. At publication an attestation may be at most 240 seconds old (MAX_ATTESTATION_AGE) and never future-dated.
73
+ Epoch sources are recipes in an owner-managed, append-only registry: a canonical request (its keccak256 is the signed query hash), a data template fixing the exact signed bytes and the gateway request body. registerRecipe appends the next id (0–255, event RecipeRegistered with the full definition); getRecipe(id) and recipeCount() read them; a recipe never changes. Built-in recipes: 0 Hyperliquid BTC daily notional volume, 1 dRPC Ethereum block hash, 2 TickerLayer BTCUSD last trade, 3 TickerLayer ETHUSD last trade, 4 Nodary ETH/USD, 5 dRPC Base block hash (BUILTIN_EPOCH_RECIPES). A catalog is 1–10 ordered sources, each a registered recipe with its signer; the initial catalog is recipes 0–3, and the owner schedules others. Both networks now use the five-source catalog [0,1,2,4,5]: Arc Testnet from epoch 966 and Arc Mainnet from epoch 848. Read catalogAt(epochId) for the catalog an epoch actually used. The epoch anchor selects one source; fallback attempt n (1 to count−1) is the source n positions later, valid only from n × 20 blocks into the epoch (sourceCountAt, getEpochFallbackSelection, fallbackOpensAt, commitEpochFallback). replayEpochCommitment derives the attempt from the committed source and rejects a commit block before its window. Signed data must match the recipe's data template exactly (matchesDataTemplate; opcodes LITERAL 0x01, HEX 0x02, DECIMAL 0x03, INTEGER 0x04; README Data templates) and is at most 128 bytes; preserve the entire exact signed response. Signatures establish wrapper provenance, not unbiased upstream data. At publication an attestation may be at most 240 seconds old (MAX_ATTESTATION_AGE) and never future-dated.
70
74
 
71
- Signer catalogs are per epoch. The registry owner can schedule a replacement with scheduleCatalog(signers, fromEpoch) at least two epochs ahead (event CatalogScheduled). A new schedule replaces a version that has not taken effect yet, which can return the next epoch to the previous catalog; the current epoch, prepared snapshots and open requests keep their signers. Replay must use signersAt(epochId) or the CatalogScheduled history without replaced versions as epoch.catalog.signers, which replayEpochCommitment binds to the record's catalogHash, while configuration.catalogHash stays the initial catalogHash() bound into protocolConfigurationHash.
75
+ Catalogs are per epoch. The registry owner can schedule a replacement with scheduleCatalog(recipes, signers, fromEpoch) at least two epochs ahead (event CatalogScheduled(fromEpoch, catalogHash, recipes, signers)). A new schedule replaces a version that has not taken effect yet, which can return the next epoch to the previous catalog; the current epoch, prepared snapshots and open requests keep their catalog. Replay must use the epoch's catalogAt(epochId) view, turned into epoch.catalog with resolveEpochCatalog, or the CatalogScheduled history without replaced versions; replayEpochCommitment binds it to the record's catalogHash, while configuration.catalogHash stays the initial catalogHash() bound into protocolConfigurationHash. Replay uses BUILTIN_EPOCH_RECIPES unless epoch.catalog.recipeBook supplies definitions; for other recipes read them with readEpochRecipes (getRecipe, query hash checked). Every published Arc epoch so far used built-in recipes. Both networks now run the recipe-registry implementation this SDK describes, behind unchanged proxies; always confirm against the manifest.
72
76
 
73
77
  Decode epoch evidence using its trusted registry/event context and decodeEvidencePacket for FulfillmentEvidence. Proof evidence is 416 bytes; a single fulfillRandomness call is 452 calldata bytes. Supply independently trusted successful receipts, proxy implementation history, source/publication/request/target blocks and timestamps, initialized key/configuration and original signed packets. Compare both event and stored transcript commitments. Decoding and mapping alone do not verify origin; replay does not authenticate RPC or establish inclusion.
74
78
 
75
- Both service contracts use atomically initialized D20Proxy endpoints with owner-authorized UUPS upgrades and two-step ownership; renounceOwnership reverts on both. Implementations are locked against initialization. The owner can rotate committer and fee recipient, adjust the keeper share, tune bounded pricing, lower the refund ratio for future requests and schedule future catalogs; no setter rewrites a request, a published epoch or the VRF key. Upgrade authority is trusted. Verify the implementation history of BOTH coordinator and registry; stable proxy addresses alone do not identify executed code. Check it when you integrate and again whenever the deployment manifest records an upgrade or a proxy emits Upgraded(implementation); a mismatch with the manifest means stop and review before sending more requests. Operator pins stop processing on unreviewed changes while preserving recovery data.
79
+ Both service contracts use atomically initialized D20Proxy endpoints with owner-authorized UUPS upgrades and two-step ownership; renounceOwnership reverts on both. Implementations are locked against initialization. The owner can rotate committer and fee recipient, allow up to four backup committers (they publish epochs under the committer's rules and earn the keeper share of the requests they serve themselves), register recipes, adjust the keeper share, tune bounded pricing, lower the refund ratio for future requests and schedule future catalogs; no setter rewrites a request, a published epoch or the VRF key. Upgrade authority is trusted. Verify the implementation history of BOTH coordinator and registry; stable proxy addresses alone do not identify executed code. Check it when you integrate and again whenever the deployment manifest records an upgrade or a proxy emits Upgraded(implementation); a mismatch with the manifest means stop and review before sending more requests.
76
80
 
77
81
  The contracts have not had an external security audit; the coordinator's "Prototype: not audited or validated on Arc" source comment is unchanged because the source is part of deployed bytecode metadata, and the service is live on Arc Mainnet. On Arc Mainnet the owner of both proxies is the DAO treasury Safe 0xB57f656149749eff6b496dF090336491f977E744. The VRF key holder can withhold a proof, which leads to a refund, but cannot substitute another result for a fixed seed.
78
82
 
79
83
  ## Service boundaries
80
84
 
81
- Always configure the actual chain explicitly; there is no implicit Arc network default. Take proxy addresses and code hashes from the public deployment manifest for that chain (https://d20dao.org/deployments/arc-mainnet.json or https://d20dao.org/deployments/arc-testnet.json) and confirm the coordinator implementation exposes quoteFee/quoteFeeAt before live requests. Healthy process status does not guarantee a particular request's timely fulfillment.
85
+ Always configure the actual chain explicitly; there is no implicit Arc network default. Take proxy addresses and code hashes from the public deployment manifest for that chain (https://d20dao.org/deployments/arc-mainnet.json or https://d20dao.org/deployments/arc-testnet.json) and confirm the coordinator implementation exposes quoteFee/quoteFeeAt before live requests. Nothing guarantees a particular request's timely fulfillment, so always handle expiry.
82
86
 
83
- This SDK holds no signer or bot keys, runs no keeper/prover and exposes no operator API. Optional Telegram access is disabled by default and limited to read-only /status and /keeper in the configured operator chat. Those commands cannot alter configuration or send transactions. Docker provisioning, upgrades, funding and publishing are separate operator actions, not consequences of SDK integration.
87
+ This SDK holds no signer keys, runs no keeper or prover and exposes no service API. Installing it neither authorizes nor performs anything on chain.
84
88
 
85
89
  ## Examples
86
90
 
87
- examples/DiceConsumer.sol (installed at @d20dao/vrf-sdk/examples/DiceConsumer.sol): player-paid d20 request forwarding msg.value, player as refund address, refund hook. contracts/examples/MiningRandomnessConsumer.sol (source protocol/contracts/examples/): abstract claim consumer paying quoteFee from its balance, one request per claim, candidate seeds from the stored word. The README's Recommended payment pattern shows D20Game, which pays the exact quote and returns change. https://github.com/d20dao/randomizer-demo: complete dapp with a consumer that stores the latest results onchain and a UI that requests, waits for and displays them.
91
+ Three self-contained consumers ship in the package, installed at @d20dao/vrf-sdk/examples/<name>.sol. Each is sixty to seventy lines, takes the coordinator proxy in its constructor, authenticates the callback through D20VRFConsumer, rejects a request id it did not issue or already finished, and reads its outcome from getMappedResult instead of recomputing it. Copy one and edit it; do not import them.
92
+
93
+ - DiceConsumer.sol: one d20 per player. Reads the exact quoteFee inside the requesting transaction, pays it through the D20VRFRequests helper, returns the change and names the player as refund address.
94
+ - RaffleConsumer.sol: one winner from a list. Closes entry before requesting, hashes the frozen entrant list into clientSeed as an on-chain commitment, maps the winning index with ChooseOne and serves exactly one draw; if the request expires, its refund reaches _onRefund and the draw can be sent again for the same frozen list.
95
+ - LootDropConsumer.sol: a weighted drop. Requests a NumberRange draw over the total weight instead of reducing the word itself, stores the word in the callback and walks the cumulative weights on read.
96
+
97
+ https://github.com/d20dao/randomizer-demo: complete dapp with a consumer that stores the latest results onchain and a UI that requests, waits for and displays them. skills/d20-consumer/assets/RandomnessConsumer.sol in https://github.com/d20dao/skills shows raw, mapped and shuffle requests in one contract.
88
98
 
89
99
  ## Optional refund notification
90
100
 
91
- After refundRequest has paid the fixed refund address or recorded its refund credit, the coordinator calls `onRefund(requestId)` on the original consumer. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
101
+ After refundRequest has paid the fixed refund address or recorded its refund credit, the coordinator calls `onRefund(requestId)` on the original consumer. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state, as RaffleConsumer does to allow a new draw; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
92
102
 
93
103
  The first attempt forwards 100,000 gas. A reverting or gas-exhausting hook cannot undo the fee settlement. After failure, `retryRefundCallback(requestId, gasLimit)` retries the notification without another payment; successful delivery is recorded by `refundCallbackDelivered(requestId)`. Refund/retry needs sufficient outer gas (see the recovery gas figures above). Never request new randomness from within either callback; use a separate application transaction.