PROVABLY FAIR

NOBODY PICKS THE RESULT.
NOT EVEN US.

Every pull, fight and Party draw runs commit, VRF, reveal. The seed is committed before you stake, signed by on-chain randomness after entries lock, and revealed so you can replay the result bit for bit.

1 · COMMITBefore staking closes we publish sha256(seed). It is locked in: changing the seed would change the hash.
2 · VRFORAO VRF signs the roll on Solana after all entries lock. Ungrindable, verifiable on chain, zero extra transactions.
3 · REVEALThe seed goes public. Hash it, compare it to the commit, and re-run the roll. Same result, every time.
VERIFY A RESULT
A BATTLE ID OPENS THAT BATTLE'S VERIFY PANEL: COMMIT, ROLLS, SEED AND A STEP-BY-STEP RECOMPUTE. A ROUND NUMBER OPENS THE PARTY ROUND HISTORY WITH PER-ROUND VERIFICATION.
THE PVP FIGHT SPEC · v1

HOW A PVP FIGHT IS COMPUTED

A PVP fight is a pure function of one seed, and that seed is derived entirely from data already committed on-chain. You can re-derive the seed and replay the whole fight yourself, swing by swing, and land on the same winner who got paid.

STEP 1 · THE SEED COMES FROM THE ON-CHAIN ROLLS

Every roll in the battle is a Collector Crypt VRF output (ECVRF, RFC 9381), each one individually provable on the public verifier and created only after all entries were locked, so nobody, us included, can predict or grind it. We hash the battle id together with every roll into one fingerprint:

seed = sha256( battleId | "seat:round:ccRoll:ccMemo" | ... for every roll, seat-then-round order )

This is the exact string the Verify panel shows as seed. Because it is built only from public VRF outputs, there is zero extra transaction and no hidden input.

STEP 2 · YOUR PULLS BECOME HP

Each seat's settled cash-out (buyback) total becomes its HP pool. In team formats the team's pools combine. Your win chance is exactly your share of the total HP, so a smaller stack still has a real, provable shot. That is the whole point of the mode.

STEP 3 · THE SEED REPLAYS THE FIGHT

The seed drives a deterministic RNG. Sides swing in round-robin order (the first swing is a seeded pick). The rules, tuned by simulation so the win rate tracks the HP share:

  • Accuracy: each swing hits with probability 0.75, otherwise it is a miss.
  • Damage: on a hit, uniform in [1 … maxHit] where maxHit = C · pairPool · (2 · targetHP₀ / pairPool)^γ. The smaller stack swings proportionally harder.
  • Crit: a top-15% damage roll is labelled a crit. It is a label only, never a multiplier, so it cannot distort the tuned balance.
  • Bounded: sudden-death escalation past a swing threshold caps every fight (~13-24 swings) and the formula is scale-invariant, so $25 packs and $10k chase pulls pace identically.
  • Result: last side standing takes the whole pot.
LIVE CONSTANTS (v1)
Accuracy0.75
C0.15
γ · 2 sides0.75
γ · 3 sides0.55
γ · 4 sides0.45
γ · 5 sides0.4
γ · 6 sides0.45
REPLAY IT YOURSELF

This is exactly what your browser runs when you open Verify on a settled fight, using the same functions the app ships. Feed it the public rolls and seat totals from any battle and it lands on the seat that got paid, or it does not, and you have caught us.

import {
  deriveFightSeedHex,
  fightSidesFromTotals,
  rngFromSeedHex,
  simulateFight,
} from '@workspace/types'; // fight spec v1

// 1) Re-derive the seed from the public on-chain rolls
const seed = deriveFightSeedHex(
  battle.id,
  battle.rolls.map((r) => ({
    seat: r.participantSeat,
    roundIndex: r.roundIndex,
    ccRoll: r.ccRoll,   // Collector Crypt VRF roll
    ccMemo: r.ccMemo,   // its memo (verify at /api/vrf/verify?memo=...)
  })),
);
// seed === battle.fightSeed   (the "Seed check")

// 2) Turn each seat's settled buyback total into HP
const sides = fightSidesFromTotals({
  seatCount: battle.seatCount,
  teamMode: battle.teamMode,
  totalsBySeat: new Map(
    battle.participants.map((p) => [p.seat, p.totalValueCents]),
  ),
});

// 3) Replay the fight deterministically from the seed
const { events, winner } = simulateFight(sides, rngFromSeedHex(seed));
// winner === the seat/side that got paid   (the "Fight check")

The battle id, rolls, seat totals and stored seed are all in the public battle snapshot (GET /battles/<id>). Each roll's ccMemo verifies independently on the VRF verifier, and each seat's NFT transfer is on Solscan, so the entire chain, from randomness to payout, is inspectable end to end.

PullBattle: pull real graded TCG cards, battle for the pot