Skip to content

Quickstart: Web (TypeScript)

Verifiable randomness in a browser app, in one line. Works with Phantom, Solflare, or any wallet-adapter signer.

Install

bash
npm install @vrand.io/web

The one-liner

ts
import { vrand } from "@vrand.io/web";

const r = await vrand.random(); // uniform float in [0, 1) — verified

What that call actually does:

  1. Generates a fresh 32-byte seed and builds a request transaction.
  2. On the wallet's first ever request: shows the protocol Terms of Use in full, then asks for a standalone signature whose sole object is the on-chain acknowledgment — explicit consent before the first request, once per wallet per network.
  3. Preflights the cost against the wallet balance (fails with real numbers before the wallet pops).
  4. The wallet signs; a bonded prover answers, usually within a slot or two.
  5. The SDK re-verifies the VRF proof locally against your own seed and requester before the promise resolves. A failed verification is a thrown error, never a number.

Vocabulary you already know

ts
await vrand.randomInt(1, 6);              // inclusive, bias-free
await vrand.pick(["red", "green", "blue"]);
await vrand.shuffle(deck);                // Fisher–Yates, rejection-sampled
await vrand.coinFlip();                   // boolean
await vrand.weighted(prizes, [80, 15, 5]); // integer weights

vrand.random((r) => spin(r));             // callback form

Each helper above is one on-chain request (one signature, one fee).

One request, many outcomes: draw()

When several outcomes belong to a single event — a shuffle plus a pick plus a die — use a Draw. One request, one fee, a deterministic stream of outcomes:

ts
const draw = await vrand.draw();
draw.int(1, 6);
draw.pick(colors);
draw.shuffle(deck);
draw.explorerUrl();      // link to the on-chain proof

Order matters (int then pick consumes different stream words than the reverse), and all outcomes of one draw become knowable the moment its VRF output lands — use separate requests for events that must not be predictable from each other.

A batch of independent numbers: randoms(count)

ts
const rs = await vrand.randoms(20); // 20 floats, 20 fees, ~4 signatures

Batched but not discounted: every number is its own on-chain request with its own fee, rent, and independently verified proof — the batching only packs them into as few signatures as possible (6 requests per transaction). In exchange, no number is derivable from another.

Sessions: one signature, then silence

ts
const session = await vrand.startSession({ lamports: 0.05e9 });
await session.randoms(60);   // zero wallet prompts
await session.random();      // still verified, still one fee each
await session.end();         // reclaim rent + sweep the rest back

startSession funds a local session key with one wallet signature; everything after signs locally. The key persists in localStorage, so a closed tab strands nothing — the next startSession recovers the key and its balance (and costs zero signatures if still funded). It is a hot key holding only what you funded: that bound is the security model, so fund play-money amounts. Full guide: Sessions — the pattern that makes on-chain games feel like normal games.

Getting your rent back: reclaim()

Every request temporarily deposits ~0.004 SOL of account rent. It is yours:

ts
await vrand.reclaim();
// closes every finished request (rent + unspent escrow back),
// slash-then-closes overdue ones — one signature per batch

Verifiable draws with receipts

ts
import { deriveWinners } from "@vrand.io/web";

const draw = await vrand.draw();
const winners = deriveWinners(draw.record.beta, entrants, 3);

deriveWinners is the published spec: winners derive deterministically from the on-chain output and the ordered entrant list, so a receipt (request address + seed + entrants) lets anyone re-fetch, re-verify, and re-derive. Weighted entries ({ name, weight }) draw without replacement; zero weights never win. See Verifying outcomes.

Configuration

The zero-config vrand export auto-detects an injected wallet on mainnet. For production, pass your own RPC endpoint (the public one rate-limits); for free experimentation, cluster: "devnet":

ts
import { createVrand } from "@vrand.io/web";

const vrand = createVrand({
  wallet,                    // any wallet-adapter / injected provider
  connection,                // your own web3.js Connection
  prover,                    // pin a specific prover PDA
  atRiskLamports: 100_000_000n, // buy a delivery guarantee (see Economics)
  onStatus: (phase) => {},   // signing → confirming → awaiting-outcome → verifying
  onTermsRequired: async (text) => showMyModal(text), // custom terms UI
});
OptionDefaultNotes
walletauto-detect (Phantom/Solflare)pass a wallet-adapter for anything else
cluster"mainnet-beta""devnet" for free test-SOL experimentation
connection / rpcUrlthe cluster's public RPCpass a provider endpoint in production (public mainnet RPC rate-limits)
proverthe VRAND fleet proverwhoever holds a prover's key can see outcomes moments early — pick one you accept
atRiskLamports0 (free tier: retry on silence)non-zero buys the slash-backed delivery guarantee
timeoutMs90 000then a typed RequestTimeout with retry guidance
onTermsRequiredbuilt-in modalrequired in non-DOM environments

Errors are typed, never silent

Every non-outcome is a named VrandWebError: TermsDeclined, InsufficientBalance (with the exact numbers), RequestTimeout (the request stays reclaimable), RequestSlashed (you were compensated if covered), VerificationFailed (never use that result). See Errors.

Node / server usage

Everything works headlessly with a keypair:

ts
import { createVrand, keypairWallet } from "@vrand.io/web";
const vrand = createVrand({
  wallet: keypairWallet(myKeypair),
  onTermsRequired: async () => true, // you are the operator agreeing
});

Apache-2.0. Live on Solana devnet.