/

Streaming feed

When you need randomness faster than a full request-callback round-trip, a RandomnessFeed PDA offers a cheap read against a value that refreshes every few seconds.

Why a feed?

The standard request_randomness_auto path produces one round per request, takes ~4 seconds end-to-end, and charges 0.002 SOL. That's fine for turn-based games. It's not fine if you have a consumer that needs randomness at sub-second resolution or that doesn't want to pay per read — an auction settlement, a high-frequency prediction market, a randomness-backed index.

The streaming feed solves this. It's a PDA account that holds a 32-byte randomness field, refreshed by a "crank" on a fixed slot cadence. Consumers pay nothing to read — they just account-load the PDA and use whatever value is there. Only the crank pays.

! Trade-off

A feed's randomness is correlated with wall clock. If your game uses the same feed reading twice inside a single second, the result will be the same. This is fine for independent queries from different users, not fine for multiple decisions inside one transaction.

Shape of a RandomnessFeed

A feed is a PDA of ["feed", authority, feed_index] bound to a specific DiceChannel. The fields you read from the consumer side:

  • randomness: [u8; 32] — most recent output.
  • round_id: u64 — which round produced it.
  • last_slot: u64 — slot at which the feed was last published. Use this for staleness checks.
  • publish_interval_slots: u64 — how often the crank expects to run.
  • is_active: bool — false if the feed is paused or closed.

The publish crank

A small off-chain loop (the crank) watches the bound channel for the next finalised round, then calls publish_feed_value once the interval has elapsed. If the crank calls too early, the program rejects with FeedPublishTooSoon. If it calls with the wrong channel, FeedChannelMismatch. See the errors reference.

You can run your own crank against your own feed — it's a few dozen lines of Rust / TS. The DICE-operated coordinator runs cranks for public feeds.

Consuming the feed

From a consumer program, a feed is just another account. You declare it in your Accounts struct, then read feed.randomness like any other field. No CPI, no callback, no channel funding.

programs/my_auction/src/lib.rs
use anchor_lang::prelude::*;
use dice::state::RandomnessFeed;

#[derive(Accounts)]
pub struct RollFromFeed<'info> {
    pub feed: Account<'info, RandomnessFeed>,
    /* ... your own accounts ... */
}

pub fn roll_from_feed(ctx: Context<RollFromFeed>) -> Result<()> {
    let feed = &ctx.accounts.feed;

    // Staleness: reject readings older than ~5 seconds (12 slots).
    let now = Clock::get()?.slot;
    require!(
        now.saturating_sub(feed.last_slot) <= 12,
        MyError::StaleFeed,
    );

    // Use the latest published randomness directly.
    let r = feed.randomness;
    let roll = (u32::from_le_bytes([r[0], r[1], r[2], r[3]]) % 6) as u8 + 1;
    // ...store/settle roll...
    Ok(())
}

Staleness guards

Because reads are free, a malicious client could try to wait for a favourable published value and call your program repeatedly with the same "stale" value. Defend with a slot-age check — reject reads older than some small multiple of the feed's configured interval.

Picking your staleness bound

A feed publishing every 8 slots (~3 s on mainnet) should be considered "fresh" for up to ~12 slots. Past that, wait for the next publish. Set a hard ceiling of ~40 slots even on low-traffic feeds.

When not to use a feed

  • Single-shot high-stakes draws. Use a per-request round; the 0.002 SOL is worth the isolation.
  • Per-user randomness. Feeds are shared — two users reading at the same slot get the same bytes. Fine for public values (global RNG), wrong for per-user seeds.