/

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
// 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
// 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:

TypeScript
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.