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. Deployment and operations

11.1 Program deployment (devnet)

From the repo root on a machine with Anchor, Rust, and Solana CLI:

npm run build:program:devnet   # anchor build --features sb-devnet
npm run sync:idl
anchor deploy --provider.cluster devnet

Current devnet program id: 9cE3x99BRH7yqjhSzjr879MGRKKoR5GAB9fqwst9Enfp (see config/devnet-program-id.txt).

Initialize or repair the pool:

npm run prepare:nusd
# SWITCHBOARD_QUEUE defaults to config/devnet-switchboard-queue.txt
SKIP_POOL_SEED=1 npm run deploy:devnet

deploy:devnet sets the Switchboard queue allowlist on the pool (must match Queue.loadDefault in the web app). Pool mint is nUSD (BdXXSiyk1GmbMjnqhyhWw4tHcYT31BY5gH8SpWUxcSw7).

If an upgrade binary is larger than the on-chain buffer:

solana program extend <PROGRAM_ID> 10240 --url devnet
anchor deploy --provider.cluster devnet --program-name donogambler

11.2 Frontend deployment (VPS)

The VPS runs only Node.js and the Next.js app:

cd app/web
npm install
npm run build
npm start
VariablePurpose
NEXT_PUBLIC_SOLANA_RPC_URLSolana RPC
NEXT_PUBLIC_NUSD_MINTnUSD mint (devnet default in lib/solana/token.ts)

Program id is read from vendored app/web/idl/donogambler.json.

11.3 IDL synchronization

After program changes:

npm run sync:idl

11.4 Deposit monitor

npm run monitor:devnet

12. User flows

12.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-pool token to nUSD 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

12.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

12.3 Passive donation (from donor's perspective)

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

13. Webhooks (incoming notifications)

Webhooks allow DonoGambler to push near-real-time events to your own HTTPS endpoints—for example dashboards, alerting, Slack bridges, or a creator-facing activity feed—without polling the blockchain.

13.1 Model

Subscriptions describe where to send events and which creators they apply to.

ScopecreatorAuthorityDelivers events for
Global / adminnullEvery creator the monitor handles (operator-facing log stream).
Per-creatorCreator’s authority pubkey (base58)Only that creator’s passive deposits and auto-flips.

Each subscription optionally filters by eventTypes. When omitted or empty, all supported event types for that scope are delivered.

Supported event types:

EventWhen
donation.receivedThe monitor detected an incoming USDC credit to the creator’s deposit token account for a specific deposit transaction (signature). Includes donor attribution, optional memo-derived fields, and optional .sol display name resolution when applicable.
flip.submittedPassive auto_flip transaction(s) submitted for that deposit cycle.
flip.settledFlip outcome decoded from logs (win/loss, partial/direct flags, payout amounts where available).
flip.failedFlip submission or outcome decoding failed (includes error summary for alerting).

Delivery runs in the deposit monitor process (app/monitor.ts). It POSTs JSON to each subscriber’s URL when an event is recorded; failed deliveries retry with backoff and persist attempt history alongside the subscription store.

Authentication (product assumption): In production, subscription APIs are only callable by authenticated callers—e.g. an operator console for global webhooks (admin session/API key/OIDC) and a creator-authenticated session tied to wallet proof for creator-scoped webhooks—so URLs and signing secrets remain private. Exact mechanism is intentionally left flexible; callers must not expose management endpoints anonymously.

13.2 Management API

The Next.js app exposes subscription management routes (/api/webhooks/subscriptions in the shipped app router layout):

MethodRole
POSTCreate or update subscription
GETList subscriptions; optional query type=events lists recent persisted events
DELETERemove subscription

Bodies (conceptual contract):

  • url — HTTPS endpoint that receives webhook POSTs.
  • creatorAuthoritynull for operator/global stream, or creator wallet base58 for creator-scoped stream.
  • eventTypes — optional array restricting which event kinds fire (donation.received, flip.submitted, flip.settled, flip.failed).
  • id — optional stable subscription id for upserts.

In production builds, these routes are protected (session, API gateway, internal network, or mTLS).

13.3 Request format (subscriber side)

Deliveries use HTTPS POST with Content-Type: application/json.

Headers (stable contract):

HeaderPurpose
x-donogambler-event-idUnique id for idempotent processing
x-donogambler-event-typeOne of donation.received, flip.submitted, flip.settled, flip.failed
x-donogambler-timestampUnix ms string used in signing

Optional HTTP signature: When DONOGAMBLER_WEBHOOK_SIGNING_SECRET is set in the deployment environment:

  • Subscriber receives x-donogambler-signature: sha256=<hex>
  • Verified as HMAC-SHA256(secret, "<timestamp>" + "." + raw_body)

Subscribers must verify signatures in production before trusting payloads.

13.4 Body shape

Each POST body is a JSON object:

{
  "id": "<uuid>",
  "type": "donation.received",
  "createdAt": "<ISO-8601>",
  "creatorAuthority": "<base58>|null",
  "data": { }
}

data is event-specific (deposit signature, amounts, donor pubkey, memo fields, .sol label, flip signatures, win/loss, etc.). The monitor and store are source of truth; fields may grow as the product evolves.

13.5 Persistence and operations

  • Webhook store: JSON backing subscriptions, emitted events, and per-delivery attempts. DONOGAMBLER_WEBHOOK_STORE_PATH overrides location (default aligns with app/.webhooks.json in the repo layout).
  • Monitor dedupe: Processed deposit signatures are persisted so restarts don’t double-flip or spam duplicate webhook events (MONITOR_STATE_PATH or default app/.monitor-processed.json).
  • Local test receiver: A development-only route /api/webhooks/receiver can append delivered payloads for smoke tests; operators replace this with external URLs in staging/production.

Operate exactly one supervised monitor instance (systemd/pm2); overlapping processes can consume deposits or publish events inconsistently.


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.