DonoGambler

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

ActorRole
DonorSigns commit_flip, funds escrow, chooses heads/tails and mode
RecipientAny Solana wallet that receives USDC on a winning flip
CreatorRegisters on-chain with deposit + destination wallets for the passive flow
Pool authorityAdmin wallet — seeds pool, withdraws liquidity
Backend authoritySigns 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

LanguageRust 2021, Anchor 0.31.1
Token standardSPL Token (classic, not Token-2022)
RandomnessSwitchboard on-demand VRF (switchboard-on-demand 0.11.3)
Program IDmhBjkoMshUzazZVC96i7U8oh8Zh3tz8czDrcEwgff1u

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

FrameworkNext.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 adaptersPhantom, Solflare via @solana/wallet-adapter-react
Switchboard client@switchboard-xyz/on-demand ^3.9.0
SwapsJupiter Aggregator v6 via server-side route handlers (/api/jupiter/quote, /api/jupiter/swap)
RPCNEXT_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.

FieldTypePurpose
authorityPubkeyPool admin — can seed, withdraw, migrate
backend_authorityPubkeyAuthorized to trigger passive auto-flips
usdc_mintPubkeySPL mint used for all pool accounting
pool_token_accountPubkeyPDA-owned ATA holding pool USDC
total_flipsu64Monotonic counter, also used as flip index for PendingFlip PDA seeds
total_auto_flipsu64Monotonic counter for PendingAutoFlip seeds
total_wonu64Number of winning flips
total_lostu64Number of losing flips
total_volumeu64Cumulative USDC staked
house_edge_bpsu16House edge in basis points (default: 500 = 5%)
max_bet_bpsu16Max bet as fraction of pool balance in bps (default: 100 = 1%)
bumpu8PDA 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.

FieldTypePurpose
creator_authorityPubkeyCreator's wallet (signs registration and profile updates)
destination_walletPubkeyWhere winnings are sent
deposit_walletPubkeyPublic address donors send USDC to
deposit_token_accountPubkeyATA of deposit wallet for USDC
is_activeboolCreator can pause/unpause
total_receivedu64Total USDC deposited by donors
total_forwardedu64Total USDC that reached destination
total_flipsu64Number of flips for this creator
total_winsu64Number of winning flips
created_ati64Registration timestamp
bumpu8PDA 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:

  1. Solvency check: pool balance ≥ 2 × stake (ensures pool can cover payout)
  2. 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:

  1. If max_flippable == 0: call auto_flip_direct — transfers full amount from deposit wallet to creator destination. No VRF needed.
  2. If max_flippable > 0: call auto_flip_commit — transfers full deposit amount to pool, immediately forwards remainder to creator destination, creates PendingAutoFlip, requests Switchboard VRF. Then after VRF resolves, call auto_flip_resolve — determines outcome, pays creator if won.
  3. auto_flip_cancel — safety valve, same 200-slot timeout pattern as donor cancel. Refunds max_flippable from 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:

  1. Frontend requests a quote from /api/jupiter/quote (server-side proxy to Jupiter v6 API, 50 bps default slippage)
  2. Frontend obtains a serialized swap transaction from /api/jupiter/swap
  3. Donor signs and sends the swap transaction — their token converts to USDC in their wallet
  4. 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

InstructionSignerEffect
initialize_poolAuthorityCreates pool PDA and vault ATA, sets authority, backend authority, mint, and default parameters
seed_poolAuthorityTransfers USDC from authority's wallet to pool vault
withdraw_liquidityAuthorityTransfers USDC from pool vault to authority's wallet
migrate_liquidity_pool_v1_to_v2AuthorityRealloc/upgrade for pool account layout changes between versions

9. Events

The program emits Anchor events for indexing and frontend consumption:

EventEmitted byKey fields
FlipCommittedcommit_flipdonor, destination, amount, mode, heads_or_tails
FlipResultresolve_flipdonor, destination, amount, won, payout, donor_refund, pool_balance_after
FlipCancelledcancel_flipdonor, amount
CreatorRegisteredregister_creatorcreator_authority, deposit_wallet, destination_wallet
AutoFlipCommittedauto_flip_commitcreator_authority, amount, max_flippable, remainder
AutoFlipResultauto_flip_resolve, auto_flip_directcreator_authority, amount, won, payout, was_partial, was_direct, pool_balance_after
AutoFlipCancelledauto_flip_cancelcreator_authority, amount

The frontend includes parseFlipResultFromLogs to decode FlipResult from transaction log lines.


10. Security considerations

10.1 Privileged keys

KeyImpact if compromised
LiquidityPool.authorityCan drain pool via withdraw_liquidity
LiquidityPool.backend_authorityCan trigger passive flips for any active creator
Creator's creator_authorityCan 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)

  1. Connect wallet on the web app
  2. Select a registered creator or paste any Solana wallet address
  3. Choose amount, flip mode (FullDonate / Cashback), and heads or tails
  4. Optionally swap a non-USDC token to USDC via Jupiter
  5. Click Flip → signs commit transaction (funds locked in escrow, VRF requested)
  6. Wait 1-2 seconds for VRF resolution
  7. Resolve transaction submits automatically → outcome displayed
  8. If VRF doesn't resolve within ~80 seconds, a Cancel button appears to reclaim funds

11.2 Creator registration

  1. Connect wallet
  2. Set destination wallet (defaults to connected wallet, can be changed)
  3. Click Register → program creates CreatorProfile, backend generates deposit wallet
  4. Receive deposit address and QR code to share publicly
  5. Dashboard shows live stats: total received, total forwarded, flip count, win rate
  6. Toggle active/inactive and update destination wallet from dashboard

11.3 Passive donation (from donor's perspective)

  1. Send USDC (or any SPL token, mainnet only) to a creator's published deposit address
  2. That's it — the system handles the rest

Glossary

TermDefinition
bpsBasis points. 10,000 bps = 100%.
PDAProgram-derived address. Deterministic, no private key — signed via seeds + bump.
VRFVerifiable Random Function. Produces unpredictable bytes with a cryptographic proof of correctness.
ATAAssociated Token Account. Deterministic SPL token account for a given wallet + mint pair.
EscrowPDA-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

EventFires when
donation.receivedMonitor 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.submittedAuto-flip transaction has been submitted for the deposit.
flip.settledFlip outcome is confirmed — includes win/loss, payout amount, partial/direct flags, and pool balance after settlement.
flip.failedFlip submission or outcome decoding failed. Includes error details for alerting.

12.2 Subscriptions

A subscription defines which events get sent where.

FieldPurpose
urlHTTPS endpoint that receives POST requests
creatorAuthorityBase58 pubkey of a specific creator, or null for a global subscription that receives events for all creators
eventTypesOptional array to filter which events fire. Omit to receive all types.
idOptional 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

MethodAction
POSTCreate or update a subscription
GETList subscriptions. Add ?type=events to list recent delivered events instead.
DELETERemove 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:

HeaderValue
x-donogambler-event-idUnique event ID for idempotent processing
x-donogambler-event-typeOne of the four event types above
x-donogambler-timestampUnix milliseconds (string)
x-donogambler-signaturesha256=<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.