Docs

Technical reference for the rippers program, the keeper and the gacha integration. For the plain-language version, see How it works.

Architecture

Buyer wallet→rippers program→Round vault (USDC)→Keeper buys pack
Collector Crypt gacha→Card NFT→Winner wallet→confirm_pull

Program

The config account is the program's settings: admin, keeper, mints, treasury, allowed swap program and the ticket split. Open it in the explorer to check every number the site shows. Program and upgrade authority both start with Rip. On mainnet the official $RIPPERS mint is set at launch; until then the config holds a placeholder and every $RIPPERS feature stays closed.

Round lifecycle

StatusEntered byMeaning
openopen_roundSelling tickets, or collecting fees for a holder drop.
lockedlast ticket / lock_dropEntries are final. draw_slot = current slot + draw_delay.
drawndrawSeed and winning_index written from the draw slot's hash.
wonsettle_ticket / settle_dropThe wallet holding the winning index is recorded.
purchasingrelease_for_purchaseExactly pack_price USDC moved to the keeper.
fulfilledconfirm_pullCard verified in the winner's wallet; gacha memo stored.
cancelledcancel_roundOnly before the draw. Ticket USDC refundable by anyone.

Accounts

AccountSeedsHolds
Config["config"]admin, keeper, USDC and $RIPPERS mints, treasury, swap program, ticket_usdc, burn_bps, treasury_bps, draw_delay, round_count
Round["round", id u64 LE]kind, status, pack_price, machine, target, sold, total_weight, burned, root, snapshot_hash, draw_slot, seed, winning_index, winner, nft, memo
Vault["vault", round]USDC token account; authority is the round PDA
Entry["entry", round, start u64 LE]buyer, start, count, refunded. Tickets start … start+count−1

Instructions

InstructionCallerChecks
initializeadminCreates config. burn + treasury < 100%, draw_delay < 500 slots.
update_configadminSame validation; can hand admin to a multisig.
open_roundadmin or keeperTicket rounds: target × ticket_usdc ≥ pack_price.
buy_ticketsanyoneCPI to the configured swap program. Vault must gain ≥ count × ticket_usdc; buyer's $RIPPERS must drop. Burns spent × burn/vault, sends spent × treasury/vault. Total ≤ max_in.
fund_dropanyoneUSDC into an open holder drop.
lock_dropkeeperVault ≥ pack price. Commits merkle root, total weight, snapshot SHA-256.
drawanyoneAfter draw_slot, while its hash is still in SlotHashes.
redrawanyoneIf the hash expired (> 500 slots), schedules a new future slot.
settle_ticketanyoneEntry range must contain winning_index.
settle_dropanyoneMerkle proof of (holder, start, weight) and range contains the index.
release_for_purchasekeeperStatus won. Moves exactly pack_price.
confirm_pullkeeperMPL Core asset owned by the winner, or an SPL token account of the winner with amount 1.
cancel_roundadminOnly open or locked.
refund_entryanyoneCancelled ticket round; pays count × ticket_usdc to the buyer once.
sweepadmin or keeperLeftovers from a fulfilled round (or cancelled holder drop) into an open round of the same kind only. This is how a fee pool worth several packs becomes back-to-back holder drops.
set_rippers_mintadminPoints the program at the official $RIPPERS mint at launch.

Parameters

NameValueNote
Ticket price$1.00$0.80 USDC to the vault (ticket_usdc = 800000)
Burn15% (1500 bps)Of the ticket, in $RIPPERS
Treasury5% (500 bps)Of the ticket, in $RIPPERS
Packpokemon_50, $5063 tickets per round
Draw delay8 slots (~3 s)Must stay under the 512-slot SlotHashes window
Holder minimum500,000 $RIPPERSPools, team and exchange wallets excluded

Verify a draw

The draw transaction emits the slot hash it read. The seed and index are pure functions of public data:

seed  = keccak256("rippers-draw" || slot_hash || round_address)
index = u64_le(seed[0..8]) mod total_weight

Recompute it from the round account:

import { Connection, PublicKey } from "@solana/web3.js";
import { decodeRound } from "./rippers.mjs";

const cx = new Connection("https://api.mainnet-beta.solana.com");
const round = new PublicKey(process.argv[2]);
const r = decodeRound((await cx.getAccountInfo(round)).data);
const index = r.seed.readBigUInt64LE(0) % r.totalWeight;
console.log({ drawSlot: r.drawSlot, index, onChain: r.winningIndex, ok: index === r.winningIndex });

Then check the slot hash in the Drawn event of the draw transaction hashes to the stored seed, and that the draw slot is after the slot the round locked in.

Verify a holder drop

  1. Download snapshots/round-<id>.json published by the keeper.
  2. Its SHA-256 must equal the round's snapshot_hash.
  3. Rebuild the tree: leaf = keccak256(0x00 || holder || start u64 LE || weight u64 LE), node = keccak256(0x01 || min(a,b) || max(a,b)). The root must equal root.
  4. The winner's row must contain winning_index in [start, start + weight).

Keeper

Polls every few seconds and moves each round one step:

Every step except release, lock and confirm is permissionless, so anyone can run a keeper if ours stops.

Gacha integration

CallUse
POST /api/generatePackplayerAddress = keeper, altPlayerAddress = winner, packType = round machine
POST /api/submitTransactionKeeper-signed purchase
POST /api/openPackReturns the card; Collector Crypt sends it to the winner
GET /api/vrf/verify?memo=Collector Crypt's own proof for the card roll

A paid pack must be opened within 2 hours or Collector Crypt refunds it, so the keeper opens immediately after paying.

Admin powers

The admin can change the keeper, treasury, swap program, ticket price, split and draw delay; cancel a round before its draw; and sweep leftovers between rounds.

The admin cannot withdraw a vault, choose a winner, change a drawn result, or move funds anywhere except another round's vault.

The swap program is the most sensitive setting: max_in and the balance checks bound what a swap can take, but a malicious program could still touch accounts the buyer passes in. Before mainnet, admin moves to a multisig with a timelock on update_config.

Errors

ErrorWhen
SwapMismatchThe swap did not deliver the USDC, or did not take $RIPPERS
SlippageTotal $RIPPERS used exceeds max_in
BadAmountZero tickets, past the round target, or invalid parameters
BadStateInstruction not allowed in the round's current status
TooEarlyDraw slot has not passed
SlotHashExpiredDraw slot fell out of SlotHashes; call redraw
NotWinnerEntry or leaf range does not contain the winning index
BadProofMerkle proof does not match the snapshot root
UnderfundedHolder drop vault below the pack price
NotWinnersCardCard account is not owned by the winner