protocol documentation

How the vaultactually works.

Custody, odds, pricing, randomness, settlement, rewards — every rule of the protocol, with the exact formulas the program computes on-chain. No mystery math, no hidden fees.

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.

Read this first. Depositor rewards come exclusively from draw fees. They are variable, depend entirely on pool activity, and are never guaranteed — a quiet pool pays nothing. A deposited card can be drawn and leave the vault at any moment. Nothing on this page is financial advice.

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 SOL),
  • 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.

selection weight
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 SOL:

cardbackingweight (∝ 1/b)odds
Grail10 SOL0.106.25%
Mid2 SOL0.5031.25%
Common1 SOL1.0062.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.

draw price
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.tscrates/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. 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. 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. 3

    Fulfill

    fulfill_draw verifies 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. 4

    Timeout safety

    If the reveal never lands within draw_timeout_slots (launch: 150 slots ≈ 60 s), the draw can be cancelled by anyone cancel_draw is 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.
The randomness account, the queue position and the winning index are all on-chain — any draw can be re-verified after the fact by anyone. On this demo deployment the oracle is a local mock with the same account layout; nothing here is a mainnet product.

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.

settlement — exact amounts (launch config, card backed 10 SOL, price 1.925)
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 fees

If 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 SOL) is the only source of depositor rewards. It splits three ways, on-chain, at fulfillment:

streamsharewho receives it
Equal split75%every active position, one equal share each — the common ground
👑 Crown10%the single position with the largest backing in the pool
Treasury15%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.

No yield is promised — ever. All three streams are funded by draw fees alone. If nobody draws, nobody earns. Rewards are not an APY, not a fixed return, and not a guarantee of any kind; past pool activity implies nothing about future activity. Your capital is at risk: your card can be drawn at any moment, and the buyback path returns less backing than you escrowed.

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.

parameterlaunch valuecompiled bound
draw margin0.05 SOL
protocol fee15%≤ 20%
crown fee10%≤ 15%
buyback ratio85%70% – 95%
keep commission1%≤ 3%
min backing0.1 SOL≥ 0.01 SOL
max backing ratio50× mean2× – 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_authorityaccept_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_lamports until 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.
deployment
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
This is a demo protocol under active development. The program is unaudited and runs on test networks with valueless test assets. Do not treat any part of it as production infrastructure until an independent security audit says otherwise.

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.