Appearance
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/webThe one-liner
ts
import { vrand } from "@vrand.io/web";
const r = await vrand.random(); // uniform float in [0, 1) — verifiedWhat that call actually does:
- Generates a fresh 32-byte seed and builds a request transaction.
- 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.
- Preflights the cost against the wallet balance (fails with real numbers before the wallet pops).
- The wallet signs; a bonded prover answers, usually within a slot or two.
- 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 formEach 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 proofOrder 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 signaturesBatched 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 backstartSession 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 batchVerifiable 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
});| Option | Default | Notes |
|---|---|---|
wallet | auto-detect (Phantom/Solflare) | pass a wallet-adapter for anything else |
cluster | "mainnet-beta" | "devnet" for free test-SOL experimentation |
connection / rpcUrl | the cluster's public RPC | pass a provider endpoint in production (public mainnet RPC rate-limits) |
prover | the VRAND fleet prover | whoever holds a prover's key can see outcomes moments early — pick one you accept |
atRiskLamports | 0 (free tier: retry on silence) | non-zero buys the slash-backed delivery guarantee |
timeoutMs | 90 000 | then a typed RequestTimeout with retry guidance |
onTermsRequired | built-in modal | required 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
});