Skip to content

Sessions — sign once, play all night

Wallet popups are where on-chain games go to die. A dice game that interrupts you with a signature request every roll isn't a game — it's a DMV queue. Sessions fix this completely:

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

const session = await vrand.startSession({ lamports: 0.05e9 }); // ONE signature
// ── from here on: zero popups ──
await session.randomInt(1, 6);   // roll
await session.randomInt(1, 6);   // roll again
await session.randoms(50);       // a whole tournament bracket
await session.draw();            // shuffle + deal, one request
await session.end();             // change swept back to the wallet

One signature funds the session. Everything after is instant — no prompts, no interruptions, no friction — until the budget runs out or you end it.

What actually happens

startSession creates a local session key in the page and moves your chosen budget to it with a single wallet signature. That key becomes the payer: it signs every request locally, instantly. Your main wallet is never asked again.

Nothing else changes. Every request still pays its normal fee, still gets its own on-chain proof, and is still verified locally before your code sees the value. Sessions remove the prompting, not the guarantees.

Why it's safe

The session key is a hot key in the page holding only what you funded — that bound is the security model. Compromise of the page risks the session's remaining budget, nothing more: your main wallet delegated nothing and signed nothing beyond one transfer. Fund it like an arcade card, not like a bank account.

  • end() reclaims the rent from every finished request, sweeps the remaining balance back to your wallet, and forgets the key.
  • Disconnecting the wallet ends the session automatically — walking away can never strand funds.
  • Closed the tab mid-game? Nothing is lost. The key persists in localStorage; the next startSession for the same wallet recovers it and its balance — and if it's still funded, resuming costs zero signatures. Resumes report themselves: session.recoveredLamports / session.toppedUpLamports.

Budget math

A request costs the flat network fee plus ~0.004 SOL of reclaimable rent that comes back at end(). So a 0.05 SOL session comfortably runs ~10 requests in flight at a time and many more over its life as rent recycles — and end() returns everything unspent. For a long game night, 0.1e9 is generous.

ts
const session = await vrand.startSession({ lamports: 0.1e9 });
console.log(await session.balance());        // lamports left to play with

The game-loop pattern

ts
let session = await vrand.startSession({ lamports: 0.05e9 });

async function onPlayerRoll() {
  const roll = await session.randomInt(1, 6);   // no popup, ~2-4s, proven
  animateDice(roll);
}

window.addEventListener("beforeunload", () => { /* nothing needed —
  the key persists; next visit resumes the session */ });

Every roll is a real on-chain request with a real VRF proof your player could audit — at the interaction cost of Math.random() plus a couple of seconds.

API

CallWhat it does
vrand.startSession({ lamports, persist? })one signature; funds (or resumes) the session key
session.random() / randomInt / pick / shuffle / coinFlip / weighted / draw() / randoms(n)everything the main client does — prompt-free
session.balance()lamports remaining
session.sessionKeythe session key's address
session.recoveredLamports / toppedUpLamportshow a resume was funded
session.end()reclaim rent, sweep back, forget the key

Full option details: Web SDK reference.

Apache-2.0. Live on Solana devnet.