DonoGambler — Technical Documentation
Status: v0.2 — April 2026 On-chain behaviour is authoritative. If this document disagrees with
programs/donogambler/src/lib.rs, trust the program.
1. Overview
DonoGambler is a Solana protocol that adds a provably fair coinflip to crypto donations. A donor stakes USDC on a coin toss — if they win, the recipient (a content creator, streamer, or any wallet) receives roughly double the original amount. If they lose, the stake is absorbed by a global liquidity pool. The pool backs all payouts and earns a 5% house edge over time.
The protocol has two modes of operation:
Active Flip — the donor connects a wallet, picks a recipient, chooses heads or tails, and flips. Two on-chain transactions (commit → resolve) with Switchboard VRF ensure the outcome is provably fair and resistant to flashloan manipulation.
Passive Flip — a creator registers and receives a public deposit address. Anyone sends USDC to that address like a normal donation. A backend monitor detects the deposit, triggers a coinflip via the smart contract, and forwards the result to the creator's private wallet. The donor doesn't interact with the program.
1.1 Why two transactions?
A single-transaction coinflip is vulnerable to flashloan attacks: an attacker wraps the flip in a flashloan and reverts the entire transaction if they lose, making it risk-free. By splitting into commit (funds locked in escrow) and resolve (randomness revealed by oracle), the donor's funds are locked in a confirmed block before the outcome exists. They cannot revert the commit, and they don't control the resolve.
1.2 Actors
| Actor | Role |
|---|---|
| Donor | Signs commit_flip, funds escrow, chooses heads/tails and mode |
| Recipient | Any Solana wallet that receives USDC on a winning flip |
| Creator | Registers on-chain with deposit + destination wallets for the passive flow |
| Pool authority | Admin wallet — seeds pool, withdraws liquidity |
| Backend authority | Signs passive auto_flip instructions on behalf of the system |
| Liquidity pool (PDA) | Holds pooled USDC, signs payouts via PDA seeds |
2. Stack
2.1 On-chain program
| Language | Rust 2021, Anchor 0.31.1 |
| Token standard | SPL Token (classic, not Token-2022) |
| Randomness | Switchboard on-demand VRF (switchboard-on-demand 0.11.3) |
| Program ID | mhBjkoMshUzazZVC96i7U8oh8Zh3tz8czDrcEwgff1u |
Build features:
sb-devnet— maps Switchboard to devnet program IDs (omit for mainnet)testing— bypasses Switchboard and derives outcomes from SlotHashes sysvar (localnet tests only, never production)
2.2 Frontend
| Framework | Next.js 16.2.1 (App Router), React 19, Tailwind CSS 4 |
| Solana client | @solana/web3.js ^1.98.4, @solana/spl-token ^0.4.14 |
| Anchor client | @coral-xyz/anchor ^0.32.1 with legacy wallet shim for provider compatibility |
| Wallet adapters | Phantom, Solflare via @solana/wallet-adapter-react |
| Switchboard client | @switchboard-xyz/on-demand ^3.9.0 |
| Swaps | Jupiter Aggregator v6 via server-side route handlers (/api/jupiter/quote, /api/jupiter/swap) |
| RPC | NEXT_PUBLIC_SOLANA_RPC_URL env var, defaults to public devnet |
2.3 Repository layout
programs/donogambler/src/lib.rs — Anchor program (all instructions, accounts, events)
app/web/ — Next.js frontend
app/web/idl/donogambler.json — Vendored IDL (re-copy from target/idl/ after anchor build)
app/monitor.ts — Backend deposit monitor for passive flow
app/keys/ — Deposit wallet keypairs (gitignored)
target/idl/donogambler.json — Generated IDL (source of truth)
target/types/donogambler.ts — Generated TypeScript types
tests/donogambler.ts — Anchor test suite
3. On-chain accounts
3.1 LiquidityPool
PDA seeds: ["pool"]
Holds all pooled USDC and global protocol state.
| Field | Type | Purpose |
|---|---|---|
authority | Pubkey | Pool admin — can seed, withdraw, migrate |
backend_authority | Pubkey | Authorized to trigger passive auto-flips |
usdc_mint | Pubkey | SPL mint used for all pool accounting |
pool_token_account | Pubkey | PDA-owned ATA holding pool USDC |
total_flips | u64 | Monotonic counter, also used as flip index for PendingFlip PDA seeds |
total_auto_flips | u64 | Monotonic counter for PendingAutoFlip seeds |
total_won | u64 | Number of winning flips |
total_lost | u64 | Number of losing flips |
total_volume | u64 | Cumulative USDC staked |
house_edge_bps | u16 | House edge in basis points (default: 500 = 5%) |
max_bet_bps | u16 | Max bet as fraction of pool balance in bps (default: 100 = 1%) |
bump | u8 | PDA bump |
3.2 CreatorProfile
PDA seeds: ["creator", creator_authority.as_ref()]
One per registered creator. Stores their deposit address, destination wallet, activity status, and cumulative stats.
| Field | Type | Purpose |
|---|---|---|
creator_authority | Pubkey | Creator's wallet (signs registration and profile updates) |
destination_wallet | Pubkey | Where winnings are sent |
deposit_wallet | Pubkey | Public address donors send USDC to |
deposit_token_account | Pubkey | ATA of deposit wallet for USDC |
is_active | bool | Creator can pause/unpause |
total_received | u64 | Total USDC deposited by donors |
total_forwarded | u64 | Total USDC that reached destination |
total_flips | u64 | Number of flips for this creator |
total_wins | u64 | Number of winning flips |
created_at | i64 | Registration timestamp |
bump | u8 | PDA bump |
3.3 PendingFlip
PDA seeds: ["flip", donor.as_ref(), flip_index.to_le_bytes()]
Created during commit_flip, consumed by resolve_flip or cancel_flip. Stores the donor's stake amount, chosen side (heads/tails), flip mode, VRF account reference, and commit slot for timeout calculations.
3.4 Flip Escrow
PDA seeds: ["escrow", pending_flip.key().as_ref()]
SPL token account that holds the donor's staked USDC between commit and resolve. The donor cannot withdraw from this — only resolve_flip (on outcome) or cancel_flip (on timeout/donor request) can move funds out.
3.5 PendingAutoFlip
PDA seeds: ["auto_flip", creator_profile.key().as_ref(), auto_flip_index.to_le_bytes()]
Same role as PendingFlip but for the passive flow. Created by auto_flip_commit, consumed by auto_flip_resolve or auto_flip_cancel.
4. Payout mathematics
4.1 Flip modes
FullDonate — on win, the recipient receives the full payout from the pool. The donor's original stake stays in the pool (it was transferred there from escrow). The donor receives nothing back.
Cashback — on win, the recipient receives the original stake amount as a donation, and the donor receives a refund equal to the payout minus the stake. Net payout from pool is the same as FullDonate.
Cashback is only available on active (donor-initiated) flips. Passive auto-flips always use FullDonate since the donor is anonymous and may have sent from an exchange wallet with no way to receive refunds.
4.2 Payout formula
Given a stake amount A and house edge H in basis points:
payout = floor(2 * A * (10000 - H) / 10000)
The house edge is applied to the gross doubled amount, not the original stake.
Example with 5% edge (H = 500): donor stakes 10 USDC → payout = floor(2 × 10 × 9500 / 10000) = 19 USDC.
4.3 Settlement by mode
FullDonate win: recipient receives payout (19 USDC) from pool.
Cashback win: recipient receives A (10 USDC), donor receives payout - A (9 USDC) from pool.
Loss (both modes): escrow transfers to pool. No payout. Pool gains the full stake.
4.4 Expected value
Per flip, the pool's expected gain:
- 50% chance: pool gains
A(loss) - 50% chance: pool loses
payout - A(win costs pool the payout minus the escrowed stake it absorbed)
With H = 500: expected pool gain per flip = A × H / 10000 = 0.5% of each stake. Over many flips, the house edge makes the pool sustainably profitable.
4.5 Max bet and solvency guards
Before accepting a commit:
- Solvency check: pool balance ≥ 2 × stake (ensures pool can cover payout)
- Max bet check: stake ≤ pool balance × max_bet_bps / 10000 (prevents a single flip from draining the pool on a win)
Both checks apply to active flips. For passive auto-flips, see section 5.
5. Passive auto-flip (creator deposits)
5.1 Graceful handling of oversized deposits
Unlike active flips which reject bets that exceed limits, passive auto-flips never reject. Every USDC deposited reaches the creator one way or another.
The program computes a split:
max_flippable = min(deposit_amount, max_bet, max_pool_can_cover)
remainder = deposit_amount - max_flippable
Three outcomes:
Full flip (max_flippable == deposit_amount): normal coinflip on the full amount.
Partial flip (0 < max_flippable < deposit_amount): flip the maximum amount, forward the remainder directly to the creator with no gamble.
Direct forward (max_flippable == 0, pool empty or too small): entire deposit forwarded straight to creator. No flip occurs.
5.2 Instruction flow
When the backend monitor detects a deposit:
- If
max_flippable == 0: callauto_flip_direct— transfers full amount from deposit wallet to creator destination. No VRF needed. - If
max_flippable > 0: callauto_flip_commit— transfers full deposit amount to pool, immediately forwardsremainderto creator destination, creates PendingAutoFlip, requests Switchboard VRF. Then after VRF resolves, callauto_flip_resolve— determines outcome, pays creator if won. auto_flip_cancel— safety valve, same 200-slot timeout pattern as donor cancel. Refundsmax_flippablefrom pool back to deposit wallet.
6. Switchboard VRF integration
6.1 Commit phase
The client (or backend for passive) generates a fresh Switchboard randomness keypair, builds a Randomness.create instruction, and includes it in the same transaction as commit_flip (or auto_flip_commit). The program CPIs RandomnessCommit::invoke and stores the returned seed_slot on the pending flip account.
6.2 Reveal phase
The client polls until current_slot > seed_slot, then builds a Switchboard revealIx (which includes oracle-signed gateway data) followed by resolve_flip. The program reads the VRF result bytes, verifies reveal_slot > 0 and seed_slot matches, then uses the first byte to determine heads (< 128) or tails (≥ 128).
6.3 Testing mode
With the testing feature flag, Switchboard CPI is skipped entirely. Randomness is derived from SlotHashes sysvar mixed with donor pubkey, timestamp, amount, and flip count. This is deterministic enough for test assertions but not suitable for production.
7. Jupiter swap integration
When a donor selects a non-USDC token on the frontend:
- Frontend requests a quote from
/api/jupiter/quote(server-side proxy to Jupiter v6 API, 50 bps default slippage) - Frontend obtains a serialized swap transaction from
/api/jupiter/swap - Donor signs and sends the swap transaction — their token converts to USDC in their wallet
- Frontend proceeds with the normal commit/resolve flip flow using the USDC
The USDC mint address is read from the on-chain pool account (LiquidityPool.usdc_mint), not hardcoded. Jupiter only operates on mainnet — on devnet, the token selector is disabled and only the pool's test mint is accepted.
For the passive flow, the backend monitor handles non-USDC deposits by swapping via Jupiter API before calling auto_flip. This is also mainnet-only.
8. Pool administration
| Instruction | Signer | Effect |
|---|---|---|
initialize_pool | Authority | Creates pool PDA and vault ATA, sets authority, backend authority, mint, and default parameters |
seed_pool | Authority | Transfers USDC from authority's wallet to pool vault |
withdraw_liquidity | Authority | Transfers USDC from pool vault to authority's wallet |
migrate_liquidity_pool_v1_to_v2 | Authority | Realloc/upgrade for pool account layout changes between versions |
9. Events
The program emits Anchor events for indexing and frontend consumption:
| Event | Emitted by | Key fields |
|---|---|---|
FlipCommitted | commit_flip | donor, destination, amount, mode, heads_or_tails |
FlipResult | resolve_flip | donor, destination, amount, won, payout, donor_refund, pool_balance_after |
FlipCancelled | cancel_flip | donor, amount |
CreatorRegistered | register_creator | creator_authority, deposit_wallet, destination_wallet |
AutoFlipCommitted | auto_flip_commit | creator_authority, amount, max_flippable, remainder |
AutoFlipResult | auto_flip_resolve, auto_flip_direct | creator_authority, amount, won, payout, was_partial, was_direct, pool_balance_after |
AutoFlipCancelled | auto_flip_cancel | creator_authority, amount |
The frontend includes parseFlipResultFromLogs to decode FlipResult from transaction log lines.
10. Security considerations
10.1 Privileged keys
| Key | Impact if compromised |
|---|---|
LiquidityPool.authority | Can drain pool via withdraw_liquidity |
LiquidityPool.backend_authority | Can trigger passive flips for any active creator |
Creator's creator_authority | Can change payout destination, toggle active status |
| Deposit wallet private keys (held by backend) | Can move funds from deposit wallets |
In production, pool authority and backend authority should be multisigs or managed through a KMS. Deposit wallet keys should be stored in AWS KMS or HashiCorp Vault, not as files on disk.
10.2 Flashloan resistance
The two-transaction commit-resolve pattern prevents flashloan attacks. Funds are locked in escrow in a confirmed block before randomness exists. The donor cannot atomically revert the commit based on the outcome.
10.3 VRF liveness
If Switchboard oracles go down, committed flips will not resolve. The cancel_flip instruction provides a safety valve — the donor can cancel at any time by signing, or anyone can cancel after 200 slots (~80 seconds) have elapsed since commit. Funds are never permanently locked.
10.4 Economic monitoring
The solvency check (pool ≥ 2 × stake) and max bet cap (1% of pool) are necessary but not sufficient for long-run solvency. Operators should monitor pool depth, payout variance, and unusual deposit patterns (particularly in the passive auto-flip remainder paths).
10.5 Jupiter proxy
The server-side Jupiter route handlers (/api/jupiter/quote, /api/jupiter/swap) should be rate-limited and authenticated in production to prevent abuse as an open relay.
10.6 Audit status
This protocol has not undergone a third-party security audit. The code is open source for review.
11. User flows
11.1 Donor (active flip)
- Connect wallet on the web app
- Select a registered creator or paste any Solana wallet address
- Choose amount, flip mode (FullDonate / Cashback), and heads or tails
- Optionally swap a non-USDC token to USDC via Jupiter
- Click Flip → signs commit transaction (funds locked in escrow, VRF requested)
- Wait 1-2 seconds for VRF resolution
- Resolve transaction submits automatically → outcome displayed
- If VRF doesn't resolve within ~80 seconds, a Cancel button appears to reclaim funds
11.2 Creator registration
- Connect wallet
- Set destination wallet (defaults to connected wallet, can be changed)
- Click Register → program creates CreatorProfile, backend generates deposit wallet
- Receive deposit address and QR code to share publicly
- Dashboard shows live stats: total received, total forwarded, flip count, win rate
- Toggle active/inactive and update destination wallet from dashboard
11.3 Passive donation (from donor's perspective)
- Send USDC (or any SPL token, mainnet only) to a creator's published deposit address
- That's it — the system handles the rest
Glossary
| Term | Definition |
|---|---|
| bps | Basis points. 10,000 bps = 100%. |
| PDA | Program-derived address. Deterministic, no private key — signed via seeds + bump. |
| VRF | Verifiable Random Function. Produces unpredictable bytes with a cryptographic proof of correctness. |
| ATA | Associated Token Account. Deterministic SPL token account for a given wallet + mint pair. |
| Escrow | PDA-controlled token account holding donor funds between commit and resolve. |
12. Webhooks
DonoGambler pushes real-time event notifications to HTTPS endpoints you configure. This powers creator dashboards, chat bot integrations, OBS overlays, alerting, and any external system that needs to react to deposits and flip outcomes without polling the blockchain.
12.1 Event types
| Event | Fires when |
|---|---|
donation.received | Monitor detects an incoming USDC transfer to a creator's deposit token account. Includes donor wallet, amount, transaction signature, and resolved .sol display name when available. |
flip.submitted | Auto-flip transaction has been submitted for the deposit. |
flip.settled | Flip outcome is confirmed — includes win/loss, payout amount, partial/direct flags, and pool balance after settlement. |
flip.failed | Flip submission or outcome decoding failed. Includes error details for alerting. |
12.2 Subscriptions
A subscription defines which events get sent where.
| Field | Purpose |
|---|---|
url | HTTPS endpoint that receives POST requests |
creatorAuthority | Base58 pubkey of a specific creator, or null for a global subscription that receives events for all creators |
eventTypes | Optional array to filter which events fire. Omit to receive all types. |
id | Optional stable ID for upserts |
Global subscriptions are intended for operators (monitoring, logging). Per-creator subscriptions are intended for creator-facing tools (dashboards, chat bots, overlays).
12.3 Management API
| Method | Action |
|---|---|
POST | Create or update a subscription |
GET | List subscriptions. Add ?type=events to list recent delivered events instead. |
DELETE | Remove a subscription |
These endpoints must be authenticated in production — the exact mechanism (API key, session auth, mTLS) is left to the operator.
12.4 Delivery format
Each event is delivered as an HTTPS POST with Content-Type: application/json.
Headers:
| Header | Value |
|---|---|
x-donogambler-event-id | Unique event ID for idempotent processing |
x-donogambler-event-type | One of the four event types above |
x-donogambler-timestamp | Unix milliseconds (string) |
x-donogambler-signature | sha256=<hex> HMAC signature (present only when DONOGAMBLER_WEBHOOK_SIGNING_SECRET is set) |
Signature verification: compute HMAC-SHA256(secret, "<timestamp>.<raw_body>") and compare against the signature header. Always verify in production before trusting payloads.
Body:
{
"id": "uuid",
"type": "donation.received",
"createdAt": "2026-04-26T14:30:00.000Z",
"creatorAuthority": "<base58 pubkey or null>",
"data": { ... }
}
The data object varies by event type and contains fields like transaction signatures, amounts, donor pubkeys, flip outcomes, and payout details. Fields may be added as the product evolves — subscribers should ignore unknown fields.
12.5 Retry and persistence
Failed deliveries are retried with exponential backoff. Delivery attempts are persisted alongside the subscription store.
The webhook store (subscriptions, emitted events, delivery history) is a JSON file at app/.webhooks.json by default, configurable via DONOGAMBLER_WEBHOOK_STORE_PATH.