Channel setup
A channel is a long-lived PDA that tells DICE which program to call back and which wallet holds the pre-paid round budget. You create it once and fund it whenever it runs low.
What is a channel?
A DICE channel stores three pieces of per-dApp config:
- Your program's pubkey — DICE CPIs into this address when a round finalises.
- An upper bound on nodes per round (
max_nodes), so a channel can't be drained by griefers requesting 50-node rounds. - A small SOL balance used to pay for rounds. Every request debits
0.002 SOL; you top up when it gets low.
The channel address is a PDA of ["channel", authority, channel_index]. Your deploy script computes it, creates it, and saves the pubkey for request_randomness_auto to pass later.
One-time setup script
The canonical pattern is an Anchor script that runs initChannel the first time. It's idempotent — if the channel already exists the call is a no-op.
// scripts/init-dice-channel.ts
// Run ONCE, at deploy time. Idempotent — rerunning does nothing if the
// channel already exists.
import * as anchor from "@coral-xyz/anchor"
import { PublicKey, Keypair, Connection } from "@solana/web3.js"
import { Dice } from "../target/types/dice"
import { MyGame } from "../target/types/my_game"
async function main() {
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
const dice = anchor.workspace.Dice as anchor.Program<Dice>
const myGame = anchor.workspace.MyGame as anchor.Program<MyGame>
// Channel index: use 0 unless you need multiple channels per program.
const channelIndex = 0
const maxNodes = 16 // upper bound on per-request node_count
// The coordinator pubkey — devnet. For mainnet this will be different;
// always confirm against the Explorer before copying into production code.
const coordinator = new PublicKey(
"3df8FZoosdv3mrYwWS82TEqQps97qAdmnnijUNhz6tp9",
)
await dice.methods
.initChannel(channelIndex, maxNodes, myGame.programId, coordinator)
.accounts({
authority: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc()
console.log("Channel created for", myGame.programId.toBase58())
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
ℹ Where does the coordinator pubkey come from?
The DICE coordinator is the off-chain daemon that observes the channel and drives rounds. Its pubkey is published by operations and visible on the explorer. On devnet, this is a stable, well-known key.
Funding
A fresh channel has zero balance. It needs SOL before any request_randomness_auto will succeed — if not, that call fails with EscrowInsufficient. Top up with fundChannel:
// scripts/fund-dice-channel.ts
import * as anchor from "@coral-xyz/anchor"
import { LAMPORTS_PER_SOL } from "@solana/web3.js"
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
const dice = anchor.workspace.Dice as anchor.Program
// 1 SOL buys ~500 rounds at 0.002 SOL each.
const amount = BigInt(1 * LAMPORTS_PER_SOL)
await dice.methods.fundChannel(amount).rpc()
console.log("Channel topped up with", amount.toString(), "lamports")
You can also fund from any wallet, not just the channel authority. This is useful for ops — an automation job can refill every channel when a balance threshold trips.
How much to fund?
- Dev / test — 0.1 SOL funds ~50 rounds. Enough for a week of integration work.
- Production — 1 SOL funds ~500 rounds. Add a watcher that refills when the channel drops below 0.05 SOL.
Wait — declare_id again?
No. You did that already in the Quickstart. The thing to double-check here is that myGame.programId in the init script above matches the declare_id!(...) in your lib.rs. If those disagree, Anchor will happily deploy your program but every round will land callbacks on a different address — and the CPI will fail silently.
! Common mistake
Regenerating your keypair after calling initChannel. The channel stores the old program id; your new program can never receive a callback until you either redeploy at the original address or call a re-target instruction.
Sanity check
Three lines of TypeScript confirm everything is wired up correctly:
const channel = await dice.account.diceChannel.fetch(channelPubkey)
console.log("callback_program:", channel.callbackProgramId.toBase58())
console.log("balance_lamports:", channel.balance.toString())If callback_program_id matches declare_id!, and balance is non-zero, you're ready to request randomness.