Subscriptions (prepaid credit)
Pay for randomness the way you'd pay for an API plan. You top up a subscription once, authorize your contracts, and they request randomness without sending a fee. Each request is deducted from the prepaid balance.
This is an addition to pay-per-request. A contract can use either model, or both.
| Pay per request | Subscription | |
|---|---|---|
| Who pays | Whoever triggers the request | The subscription owner |
| Payment per request | HYPE as msg.value, or an approved GENESIS transfer | None |
| Good for | Users paying for their own actions | Free-to-play games, apps that sponsor their users, backends |
How it works
- Create:
createSubscription()makes you the owner and returns asubId. - Top up:
fundSubscription(subId, token, amount)in HYPE (address(0), sent as value) or GENESIS (approve the coordinator first). Anyone can top up any subscription. - Authorize:
addConsumer(subId, yourContract). Up to 100 consumers per subscription. - Request: your contract calls
requestRandomWordsWithSubscription(subId, keyHash, confirmations, callbackGasLimit, numWords)and sends nothing. The fee is deducted from the subscription balance.
Which token is charged
- After launch, when the subscription holds enough GENESIS for the request, it's charged in GENESIS, which is the cheaper option.
- Otherwise it's charged in HYPE.
- If neither balance covers one request, the request reverts with
InsufficientSubscriptionBalance.
Fees are priced for the subscription owner, so the owner's staking discount applies to every request.
Example: a free-to-enter raffle
contracts/examples/RaffleGame.sol is a complete game where players never pay for randomness:
contract RaffleGame is GenesisVRFConsumerBase, Ownable {
uint256 public subscriptionId;
function enter() external { /* free: no payment */ }
function draw() external returns (uint256 requestId) {
// ... checks: entry window closed, at least two players
requestId = _requestRandomWordsWithSubscription(subscriptionId, keyHash, 1, 200_000, 1);
}
function fulfillRandomWords(uint256, uint256[] memory words) internal override {
winner = players[words[0] % players.length];
}
}Anyone can trigger draw() once a round's entry window has closed. That spends at most one request of credit per round, so the owner's cost stays bounded. Rounds with fewer than two players are extended instead of drawn.
Run it locally against the dev chain:
cd node
npx tsx src/raffle-demo.tsFair Selection with credit
Organizers who own a subscription can draw without sending a fee:
coordinator.addConsumer(subId, address(fairSelection)); // once
fairSelection.drawWithSubscription(selectionId, subId); // the caller must own subIdFair Selection checks that the caller owns the subscription, so nobody can draw against someone else's credit.
Managing a subscription
| Call | Who | Effect |
|---|---|---|
getSubscription(subId) | anyone | Owner, consumers, request count, HYPE and GENESIS balances |
quoteSubscriptionRequest(subId) | anyone | Token and fee the next request will be charged |
withdrawSubscription(subId, token, amount, to) | owner | Withdraw unused credit at any time |
removeConsumer(subId, consumer) | owner | Revoke a contract |
requestSubscriptionOwnerTransfer / acceptSubscriptionOwnerTransfer | owner, then new owner | Two-step handover |
cancelSubscription(subId, to) | owner | Refund everything and close |
Because requests are charged when they're made, withdrawing never affects requests already in flight.
From TypeScript
const subId = await client.createSubscription();
await client.fundSubscription(subId, ZeroAddress, parseEther("0.05")); // HYPE
await client.vrfCoordinator.addConsumer(subId, myContract);
const info = await client.getSubscriptionInfo(subId);
// info.nativeBalance, info.genesisBalance, info.requestsLeft, info.nextCharge