Docs
Technical reference for the rippers program, the keeper and the gacha integration. For the plain-language version, see How it works.
Architecture
- rippers program holds every round's USDC in a PDA vault, records tickets, runs the draw and releases only the pack price.
- Swap: tickets are paid in $RIPPERS;
buy_ticketsCPIs into the configured swap program (Jupiter on mainnet) with the vault as the destination and measures the result. - Keeper: a bot that calls the permissionless steps, buys the pack from Collector Crypt with the released USDC and confirms the card reached the winner.
- Collector Crypt: sells the pack, rolls the card with its own VRF, and sends it to the winner via
altPlayerAddress.
Program
RipnaL19qSC65FYa64ttfQr7sRMrKSXp2AXRdHMbQmvRip2Q84iDVJows9E5W5VbP1tuFKBvrHyaaPit6BStrhRip8LZfmRQrQfzJFjEfYPWRM1DzkcDCXH6hmgwsGo13Solana mainnetEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1vJUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 Jupiter v6Anchor 0.31 · RustThe 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
| Status | Entered by | Meaning |
|---|---|---|
open | open_round | Selling tickets, or collecting fees for a holder drop. |
locked | last ticket / lock_drop | Entries are final. draw_slot = current slot + draw_delay. |
drawn | draw | Seed and winning_index written from the draw slot's hash. |
won | settle_ticket / settle_drop | The wallet holding the winning index is recorded. |
purchasing | release_for_purchase | Exactly pack_price USDC moved to the keeper. |
fulfilled | confirm_pull | Card verified in the winner's wallet; gacha memo stored. |
cancelled | cancel_round | Only before the draw. Ticket USDC refundable by anyone. |
Accounts
| Account | Seeds | Holds |
|---|---|---|
| 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
| Instruction | Caller | Checks |
|---|---|---|
initialize | admin | Creates config. burn + treasury < 100%, draw_delay < 500 slots. |
update_config | admin | Same validation; can hand admin to a multisig. |
open_round | admin or keeper | Ticket rounds: target × ticket_usdc ≥ pack_price. |
buy_tickets | anyone | CPI 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_drop | anyone | USDC into an open holder drop. |
lock_drop | keeper | Vault ≥ pack price. Commits merkle root, total weight, snapshot SHA-256. |
draw | anyone | After draw_slot, while its hash is still in SlotHashes. |
redraw | anyone | If the hash expired (> 500 slots), schedules a new future slot. |
settle_ticket | anyone | Entry range must contain winning_index. |
settle_drop | anyone | Merkle proof of (holder, start, weight) and range contains the index. |
release_for_purchase | keeper | Status won. Moves exactly pack_price. |
confirm_pull | keeper | MPL Core asset owned by the winner, or an SPL token account of the winner with amount 1. |
cancel_round | admin | Only open or locked. |
refund_entry | anyone | Cancelled ticket round; pays count × ticket_usdc to the buyer once. |
sweep | admin or keeper | Leftovers 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_mint | admin | Points the program at the official $RIPPERS mint at launch. |
Parameters
| Name | Value | Note |
|---|---|---|
| Ticket price | $1.00 | $0.80 USDC to the vault (ticket_usdc = 800000) |
| Burn | 15% (1500 bps) | Of the ticket, in $RIPPERS |
| Treasury | 5% (500 bps) | Of the ticket, in $RIPPERS |
| Pack | pokemon_50, $50 | 63 tickets per round |
| Draw delay | 8 slots (~3 s) | Must stay under the 512-slot SlotHashes window |
| Holder minimum | 500,000 $RIPPERS | Pools, 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
- Download
snapshots/round-<id>.jsonpublished by the keeper. - Its SHA-256 must equal the round's
snapshot_hash. - 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 equalroot. - The winner's row must contain
winning_indexin[start, start + weight).
Keeper
Polls every few seconds and moves each round one step:
lockedand past the draw slot →draw(orredrawif expired)drawn→ finds the winning entry or proof →settle_*won→release_for_purchasepurchasing→ buy the pack from Collector Crypt for the winner →confirm_pull- holder drop holds a pack's worth of fees → fresh snapshot →
lock_drop(one random winner per pack) - finished round with leftover USDC →
sweepinto the open round of the same kind, so the next drop starts right away - always keeps one open round of each kind
Every step except release, lock and confirm is permissionless, so anyone can run a keeper if ours stops.
Gacha integration
| Call | Use |
|---|---|
POST /api/generatePack | playerAddress = keeper, altPlayerAddress = winner, packType = round machine |
POST /api/submitTransaction | Keeper-signed purchase |
POST /api/openPack | Returns 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.
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
| Error | When |
|---|---|
SwapMismatch | The swap did not deliver the USDC, or did not take $RIPPERS |
Slippage | Total $RIPPERS used exceeds max_in |
BadAmount | Zero tickets, past the round target, or invalid parameters |
BadState | Instruction not allowed in the round's current status |
TooEarly | Draw slot has not passed |
SlotHashExpired | Draw slot fell out of SlotHashes; call redraw |
NotWinner | Entry or leaf range does not contain the winning index |
BadProof | Merkle proof does not match the snapshot root |
Underfunded | Holder drop vault below the pack price |
NotWinnersCard | Card account is not owned by the winner |