Skip to content

Playing without the app

The Mini App is optional. Every action in BurnBola is a public instruction on a Solana program — callable from your own code, a script, or any tool that can send Solana transactions. This page is the map for the technically inclined.

Program ID

The program ID ships inside the app bundle alongside the IDL — together they give you typed access to everything below.

The shape of the program

BurnBola is an Anchor program. Its IDL (the machine-readable interface) ships with the app bundle and decodes every account and instruction; with the IDL and @coral-xyz/anchor you get typed access to everything below.

State lives in program-derived accounts (PDAs), keyed by seeds:

AccountSeedsWhat it holds
Lottery config"lottery_config"global settings, pause flag, treasury
Game"game", game_id (u64 LE)phase, round, counts, deadlines, exit price, on-chain showcase (name + social handles)
Bank vault"bank_vault", game_idthe pot
Jackpot vault"jackpot_vault", game_idthe sealed jackpot
Ticket registry"ticket_registry", game_id2-bit status per ticket (the burn bitmap)
Owner record"owner_record", game_id, walletyour ticket ids in that game
Promoter pool"promoter_pool", game_idthe game's escrowed 9% promoter pool
Contribution"contribution", game_id, promotertickets credited to one promoter in that game
Game history"game_history", game_idfinal outcome, winner, totals
Ticket listing"ticket_listing", game_id, ticket_id (u64 LE)a P2P ticket offer: price, seller, the exit window it is valid for
Finalist pass"finalist_pass", game_id, place (1 byte)the champion pass itself — a Metaplex Core asset
Pass listing"listing", game_id, placea pass offer: price, seller
Entry round"entry_round", game_idhow many mid-game entries this round has used, against its quota

Player instructions

Every fee below goes to the lottery. There is no hidden spread: the numbers are constants in the contract, and each one is listed here.

InstructionWhat it doesFee
initialize_gamecreate a game (pays rent, sets the on-chain showcase) — REQUIRES the creator's buy_ticket in the same transaction
buy_ticketbuy one ticket at the fixed price during Sale; optional promoter (referrer) pubkey1% (a further 9% is escrowed into the game's promoter pool)
enter_gamejoin a running game in an exit window at the fair live price (per-round quota)10% on top of the price: 9% to the promoter pool, 1% to the lottery
exit_ticketsell one live ticket back at the round's fixed exit price1% — you receive 99%
list_ticketoffer one of your live tickets for sale (exit window only)
buy_listed_ticketbuy someone's listed ticket; the ticket id moves to you1%, paid by the seller — the buyer pays exactly the listed price
cancel_ticket_listingwithdraw your own ticket offer; rent comes back to you
close_stale_listinganyone may sweep an offer whose exit window has passed; rent always returns to the original seller
postpone_startnamed-game creator delays the start by one sale-window (max 2, last-window only)
settle_promoter_poolpermissionless: push a finished game's promoter pool to its promoters

Champion passes and the tournament

A pass is a Metaplex Core NFT, minted to you by the contract but owned by your wallet like any other NFT.

InstructionWhat it doesFee
claim_finalist_passthe winner (place 1) or runner-up (place 2) of a finished game mints their pass
list_passoffer a pass on the in-app market; the pass moves into program escrow while listed
update_pricechange your listed price (no asset movement)
cancel_listingtake a pass back out of escrow
buy_passbuy a listed pass1%, paid by the seller
enter_super_finalenter a pass into the open tournament — this burns the pass in exchange for a shot at the pot

Sending a pass is not one of our instructions

Because a pass is an ordinary Core asset, you can transfer it to any address without touching our program at all — a plain Metaplex Core TransferV1 does it, and the app's "Send" button is exactly that. No fee is charged, and the contract never sees it.

The collection also declares a 10% royalty, but with no enforcement rule set: it is a request that an external marketplace may honour or ignore. So our 1% applies only to sales made through our own market. We would rather state this plainly than let you discover it. A listed pass cannot be sent, since escrow holds it while the offer stands.

Tickets are the opposite case: a ticket is not a token but an entry in a program-owned account, so it cannot move except through buy_listed_ticket, and its 1% cannot be avoided.

Crank instructions — what drives a game

These move a game through its phases. A Keeper sends them automatically for every game; the contract also lets any ticket holder of a game send the next step itself, as a permission-less fallback so a game can never depend on one server being up.

InstructionWhat it does
advance_phasemove the game to its next legal phase (start, open/close exit windows, finish)
commit_vrf / settle_vrfbind an oracle randomness account (Switchboard / ORAO), then verify its reveal into the round seed
burn_batchexecute a slice of the round's burns
finalize_winner_payoutpay the winner, record history
cancel_idle_sale / settle_cancelled_game / refund_ticketthe refund path for stalled games
emergency_open_refund / emergency_exit_ticket / settle_emergencylast-resort refund mode if both oracle networks stay dead for ~30 days mid-round — the only refund that takes no fee
close_gamereclaim a settled game's rent for its creator

Phase legality, deadlines, and payouts are all enforced on-chain — a call that isn't due simply fails. In normal operation the Keeper sends all of these; the holder fallback exists only so a game can keep moving if the Keeper is ever down. (How a game runs itself.)

The randomness step is not our instruction

commit_vrf and super_commit_vrf do not create randomness — they bind a randomness account that already exists. Producing one is a separate, paid transaction to the oracle's own program, and without it the two instructions above cannot be called at all. This is the one step that is easy to miss.

js
// 1. Ask ORAO for randomness (its program, its fee — roughly 0.0003 SOL).
const seed = randomBytes(32);
await (await orao.request(seed)).rpc();
const randomnessAccount = randomnessAccountAddress(seed);   // derived from the seed

// 2. Bind it to the round WHILE IT IS STILL EMPTY.
await program.methods.commitVrf(0 /* 0 = ORAO, 1 = Switchboard */)
  .accounts({ caller, game, newRandomness: randomnessAccount, prevRandomness: null, ... })
  .rpc();

// 3. Once the oracle has filled it (a second or two), anyone may settle.
await program.methods.settleVrf().accounts({ caller, game, randomness: randomnessAccount }).rpc();

Step 2 must land before the oracle fills the account — usually a 1–2 second window. That is deliberate, and it is the core of the fairness guarantee: an account can only be bound while its value does not yet exist, so nobody can look at a number first and decide whether to use it. If you lose the race the instruction reverts; request a fresh seed and retry.

If a bound account never gets filled, commit_vrf again after the re-commit timeout, passing the stalled account as prevRandomness — the contract checks it is still empty before letting you switch. A delivered value can never be abandoned.

Driving the championship

The tournament is a small state machine on one global account (seeds: "super_game"). Read its state and round_id to know what to send.

StateWhat it meansWhat to send
0 Accumulategames are finishing and passes are being claimednothing — finalists may already enter_super_final
1 Entry openthe trigger fired: the pot is sealed and a deadline is setnothing until entry_deadline_slot passes, then super_commit_vrf
2 Draw pendinga randomness account is boundsuper_settle_vrf once the oracle fills it — this pays both pools and returns the state to Accumulate

The trigger is every 100 finished games, counted on-chain. Two rules catch people out:

  • Entry is open the whole cycle, not only during the entry window — a finalist can enter the moment they claim, and does not have to catch a narrow slot.
  • A pass belongs to one cycle. claim_finalist_pass and enter_super_final both require the game's super_cycle to equal the current round_id. A pass from an earlier cycle is a keepsake and can no longer be entered.

Same randomness step as a burn round: request an ORAO account, bind it with super_commit_vrf while it is empty, then super_settle_vrf. If both oracle networks stay dead for ~30 days, super_emergency_distribute splits the sealed pot evenly among whoever entered, so a stuck draw cannot trap the money.

InstructionWhat it does
super_commit_vrf / super_settle_vrfbind an oracle account for the champion draw, then settle it and pay the two pools
close_leaderboard_epochseal a finished epoch and push every ranked promoter their share
close_finished_recordspermissionless cleanup: return each player's record rent after a game is settled

Backstops — anyone may call these if we disappear

None of these need us, and none can be aimed anywhere but their fixed destination. They exist so that no pot can be stranded by our inaction.

InstructionWhen it becomes callable
cancel_idle_salean undersubscribed sale sat with no activity for ~30 days
force_open_super_finalno game has finished for ~30 days and no sale is open — opens the championship early so the passes that were earned still get their draw
super_emergency_distributea draw has been stuck without randomness for ~30 days — splits the sealed pot evenly among the entrants, 75/25 across the two pools
final_close_leaderboardsame idle condition — pays the standing leaderboard out
reclaim_jackpot_carrysame idle condition — the carried jackpot has no next game to seed, so it goes to the fixed revenue address

The privileged instructions — the complete list

For transparency, here is every instruction a player cannot call, and what it is bounded by. There are no others.

InstructionWhoWhat it can and cannot do
withdraw_revenueoperatormoves lottery revenue only to the configured address — the caller picks the amount, never the destination
set_pausedoperatorblocks the creation of new games; running games are untouched and still finish
set_treasury_authority · set_keeper · set_revenue_destinationupgrade authorityrotate the operator, the keeper, the revenue address
reclaim_super_prizeupgrade authoritydrains the tournament fund to the fixed revenue address, and is refused while any draw is in flight
initialize_lottery · init_sale_lock · init_super_game · init_finalist_collection · migrate_config · resize_super_gameone-time setupcreate or migrate the global accounts; they cannot run twice

What is missing from that list is the point: no instruction can change a game's outcome, take a pot, or stop a game that is already running — not for the operator, not for the upgrade authority, not for us.

Practical notes

  • Reads: all state is plain account data — getProgramAccounts over the program id enumerates every game; any explorer shows the vault balances.
  • Batching caps (transaction-size limits, measured): 10 buy_ticket per transaction, 20 exit_ticket per transaction.
  • Two leagues, two programs. Shrimp and Whale are separate deployments of the same code and share nothing on-chain — separate vaults, tournament and leaderboard. Point your client at the program id of the league you mean.
  • Ticket ids are shared between buy_ticket and enter_game: a mid-game entrant takes the next id, so ids are not "sale-only".
  • Timing is in slots, not wall-clock. Deadlines are slot numbers, so they drift against the clock as block times vary. Read the current slot, don't compute from a timestamp.

What is not in the contract

Everything else on this page is one of our instructions. These three are not, and you would otherwise find out the hard way:

ActionWhere it actually happens
Producing randomnessORAO's (or Switchboard's) own program, as a paid request. Prerequisite for every VRF instruction — see above.
Sending a pass to someoneMetaplex Core, as a plain TransferV1. Our program is not in the transaction, so no fee is taken and we never see it.
A named game's banner imageuploaded to Arweave by the creator's own client before initialize_game; the contract only stores the resulting URL. The name and social handles, by contrast, are on-chain and verified by the contract.

Everything a game needs to run and pay out is on-chain. The items above are either someone else's program or pure cosmetics — none of them can hold up a game or a payout.

Completeness

This page lists all 48 instructions of the program. That is checked mechanically against the IDL rather than by hand, in both directions: no instruction is missing, and nothing is named here that does not exist. If you find a gap, it is a bug in this page — tell us.

If you build something on top — a bot, an alternative UI, an analytics dashboard — you don't need our permission, and the contract won't know the difference.

BurnBola — every burn, every winner, on-chain.