01 · overview
One pool, two roles
shinycapital is a gacha protocol for tokenized graded Pokémon cards on Solana. Each physical slab lives in insured storage and is represented on-chain by an NFT that is a 1:1 custodial claim on that exact card. Everything else — odds, price, randomness, payouts — is computed by the program, in public.
There are two ways to play:
- Depositors place a card NFT in the pool and back it with their own SOL. Backing is the economic engine: it sets the card's odds, feeds the pool price, and earns a share of every draw fee while the card sits in the vault.
- Participants pay one pool-wide price for a draw. A verifiable random number picks a card, weighted by backing. The winner then keeps the card — or takes an instant 85% buyback in SOL.
The loop closes on itself: draw margins fund depositor rewards, backing funds buybacks, and the price is derived from the backings — there is no oracle for card prices, no admin-set rarity table, and no external yield source anywhere in the system.
02 · custody
Cards, claims and positions
A card enters the system through a whitelisted collection: the pool authority registers the custodian's NFT collection (add_collection), and only verified members of an enabled collection can be deposited. Each NFT is minted 1:1 against a physical, graded slab held in insured storage.
Depositing (deposit_position) escrows the NFT in a program-owned account and moves your SOL backing into a dedicated per-position vault. The deposit is bounded on both sides:
- backing must be at least the pool's
min_backing(launch: 0.1
), - and at most
max_backing_ratio× the pool's harmonic mean (launch: 50×), so one whale card cannot freeze the pool's pricing.
As long as the card has not been drawn, the depositor can exit at any time: withdraw_position returns the NFT, the full backing and every accrued fee in one transaction, then closes the position. There is no lock-up and no exit penalty.
Redemption of the physical card is the NFT's whole purpose: burning the claim with the custodian triggers shipment of the slab itself.
03 · odds
Weight is 1 / backing
Every active position carries a selection weight equal to the inverse of its backing. Cheaply-backed cards are common pulls; heavily-backed grails are rare. Rarity is not declared by an admin — it emerges from how much SOL depositors are willing to put behind each card.
weight(i) = SCALE / backing(i) // SCALE = 2^96, integer division odds(i) = weight(i) / Σ weight(j) // over all active positions
Worked example — a pool with three cards backed at 10, 2 and 1
:
| card | backing | weight (∝ 1/b) | odds |
|---|---|---|---|
| Grail | 10 | 0.10 | 6.25% |
| Mid | 2 | 0.50 | 31.25% |
| Common | 1 | 1.00 | 62.50% |
Weights live in a Fenwick (binary indexed) tree, so selecting a winner and updating a position are both O(log n) — at the pool's 512-slot capacity, a draw descends at most 9 nodes.
04 · pricing
Harmonic mean + flat margin
Every draw costs the same, pool-wide price: the harmonic mean of all active backings, plus a flat protocol margin. The harmonic mean — not the average — is the right statistic here: it is dominated by the cheap, high-odds cards you are most likely to pull, so the price tracks what a draw is actually worth.
H = n / Σ (1 / backing(i)) // harmonic mean, rounded UP price = H + margin // margin is flat, in lamports // example — backings 10, 2, 1 SOL: H = 3 / (0.1 + 0.5 + 1.0) = 3 / 1.6 = 1.875 SOL price = 1.875 + 0.05 = 1.925 SOL
Two implementation details matter and are mirrored bit-for-bit in the UI (lib/protocol/math.ts ↔ crates/gacha-math):
- the mean is computed in 296 fixed-point and rounded up (the participant pays it — the pool is never underpaid by a rounding),
- every amount is an integer number of lamports end to end; no float ever touches a balance. The price shown before you sign is the price the program charges, to the lamport.
request_draw also takes a max_price cap chosen by the participant: if deposits move the mean between signing and execution, a draw above your cap simply fails instead of overcharging you.
05 · the draw
Verifiable randomness, strict FIFO
- 1
Request
The participant pays the pool price into escrow and commits a Switchboard On-Demand randomness account in the same transaction. The request joins a strict FIFO queue — draws settle in the exact order they were paid, no exceptions, no reordering. - 2
Reveal
One slot later, the Switchboard oracle reveals the random value. The seed is bound to the commit slot, so neither the participant, the crank, nor the pool authority can grind or choose the outcome. - 3
Fulfill
fulfill_drawverifies the randomness account on-chain, walks the Fenwick tree (≤ 9 steps) to the weighted winner, removes the position from the active set, and splits the margin into the reward streams. The card is now the participant's to settle. - 4
Timeout safety
If the reveal never lands withindraw_timeout_slots(launch: 150 slots ≈ 60 s), the draw can be cancelled by anyone —cancel_drawis permissionless and refunds the full price to the recorded participant. Funds cannot get stuck behind a dead oracle, and a late reveal can never be accepted.
06 · settlement
Keep it, or take 85% back
The winner has a decision window of settle_timeout_slots (launch: 900 slots ≈ 6 minutes) and exactly two mutually exclusive outcomes — the program is built so no path can ever yield both:
Keep the card
settle_keep — the NFT (and its physical claim) transfers to the winner. The depositor is paid out: their full backing plus the draw principal, minus the keep commission, plus every accrued fee.
Buyback 85%
settle_buyback — the winner takes 85% of the card's backing in SOL instead; the NFT returns to its depositor, who keeps the remaining 15% plus the draw principal.
principal = price − margin = 1.925 − 0.05 = 1.875 SOL
KEEP participant → the card
depositor → (10 + 1.875) × (1 − 1%) = 11.756 SOL + accrued fees
BUYBACK participant → 10 × 85% = 8.500 SOL
depositor → the card back
+ (1.5 + 1.875) × (1 − 1%) = 3.341 SOL + accrued feesIf the window closes with no choice, settle_default (permissionless) applies the buyback: the card goes home to its depositor and the absent winner is still paid their 85% — the conservative outcome for both sides. The 1% keep commission is charged on the depositor's gross payout and is the treasury's only cut outside the margin; fees and crown accruals ride on top, commission-free. Every branch conserves lamports exactly — the settlement algebra in settle.rs sums to zero by construction.
07 · rewards
Draw fees only — three streams
Every draw's flat margin (launch: 0.05
) is the only source of depositor rewards. It splits three ways, on-chain, at fulfillment:
| stream | share | who receives it |
|---|---|---|
| Equal split | 75% | every active position, one equal share each — the common ground |
| 👑 Crown | 10% | the single position with the largest backing in the pool |
| Treasury | 15% | the protocol |
The equal stream uses a classic reward-per-share accumulator (acc_fee_per_position), so claiming (claim_fees) costs O(1) whenever you like — rewards accrue whether you touch them or not, and are paid out automatically when a position is drawn or withdrawn.
The crown belongs to whoever backs the most: any position with a strictly larger backing can seize it at any time with claim_throne. If the crown is vacant — or the crowned card is itself the one being drawn — its 10% folds into the equal split rather than stranding.
The pool also tracks Σ √backing on-chain, the accounting basis for a future native-token stream weighted by the square root of backing — favouring breadth over single whales. That stream is not live: no token exists today, and nothing here is a commitment that one will.
08 · governance
Timelocked, bounded, pausable
The pool authority can tune parameters — but only inside hard bounds compiled into the program, and only through a timelock. Every change is proposed (propose_config_update), waits out the pool's timelock (launch: ≈ 36 minutes, the protocol minimum), then is applied in a second transaction — never mid-draw, and never faster than a settlement window, so the rules cannot change under a participant who has already paid.
| parameter | launch value | compiled bound |
|---|---|---|
| draw margin | 0.05 SOL | — |
| protocol fee | 15% | ≤ 20% |
| crown fee | 10% | ≤ 15% |
| buyback ratio | 85% | 70% – 95% |
| keep commission | 1% | ≤ 3% |
| min backing | 0.1 SOL | ≥ 0.01 SOL |
| max backing ratio | 50× mean | 2× – 100× |
| config timelock | ≈ 36 min | ≥ 5 400 slots |
Even a fully compromised authority key cannot set a 100% commission, a dust backing floor or an instant rule change — the bounds are not config, they are code. Authority transfer itself is two-step (transfer_authority → accept_authority), and the pool can be paused, which blocks new deposits and draws but never blocks withdrawals or settlement.
09 · security
What protects the funds
- Verifiable randomness. The program checks the Switchboard account's discriminator, seed slot and reveal slot on-chain; a stale or foreign randomness account is rejected. Late reveals are refused — past the deadline, only cancellation remains.
- Exact escrow accounting. Every lamport a participant pays is tracked in
escrowed_lamportsuntil settled or refunded; settlement algebra conserves the total by construction. - Permissionless liveness. Fulfilling, cancelling a timed-out draw and applying the default settlement are all permissionless cranks — no single operator can hold funds hostage by going offline.
- No custodial signatures. The dApp holds no keys and signs nothing itself; every transaction is built locally and approved in your wallet.
- Bounded blast radius. Hard parameter bounds, a settlement-aware timelock, a pause switch that cannot trap funds, and a 512-position capacity per pool.
program id : Rf3zXZF3Ldgq78gMCAVQ9ojPuCNsugHTkcqfTDwotAZ network : local test validator (devnet deploy in progress) mainnet : not deployed — deliberately not wired up in the dApp audit : none yet — an independent audit is required before mainnet
10 · faq
Quick answers
Can the team change my odds after I deposit?
No. Odds are computed from backings alone — there is no rarity knob. The only levers are the bounded, timelocked config parameters above, and none of them touches selection weights.
What happens to my card if a winner takes the buyback?
It comes back to you, along with 15% of your backing and the draw principal. Your position closes; you can redeposit it with fresh backing whenever you like.
Can I lose money as a depositor?
Yes. If your card is drawn and kept you are paid backing + principal, but if the winner takes the buyback you recover only 15% of your backing (plus principal and accrued fees). Backing is risk capital, not a deposit account.
Why did my draw cost slightly more than the mean?
The harmonic mean is rounded up to the next lamport and the flat margin is added — that margin is exactly what funds depositor rewards. The price you approve in your wallet is the on-chain price.
Is there a token?
No. The pool tracks √backing accounting for a possible future stream, but no token exists, and nothing on this page promises one.
Who ships the physical card?
The custodian holding the graded slab. Burning the claim NFT triggers the release; shinycapital is not affiliated with Pokémon or any grading company.
Read enough?The pool is one click away.
