DexterDexter

DexterWagering System

How USDC wagering works in PokeDexter.

Wagering System

PokeDexter's wagering system lets players stake real USDC on Pokémon battles. This page explains the complete flow from challenge to payout.

Overview

Challenge → Accept → Deposit → Battle → Settlement
  1. Player A challenges Player B with a wager amount
  2. Player B accepts the challenge
  3. Both players deposit to a shared escrow wallet
  4. Battle happens like normal Pokémon Showdown
  5. Winner receives the pot (minus house fee) automatically

The Challenge Flow

Creating a Challenge

/wager USERNAME, AMOUNT [, FORMAT]

What happens:

  1. Server validates challenger has a connected wallet
  2. Server validates amount is within limits ($1-$100 by default)
  3. Challenge is stored in pending state
  4. Target user receives notification
  5. Challenge auto-expires after 5 minutes

Example:

/wager Gary, 10, gen9randombattle

Accepting a Challenge

/acceptwager USERNAME

What happens:

  1. Server validates accepter has a connected wallet
  2. Server generates a new escrow Keypair
  3. Both players receive deposit instructions
  4. Challenge moves from "pending" to "awaiting_deposits"

The Deposit Process

Escrow Wallet Generation

When a wager is accepted, the server creates a fresh Solana Keypair:

const escrowKeypair = Keypair.generate();
const escrowAddress = escrowKeypair.publicKey.toBase58();

This escrow wallet:

  • Is unique to this specific match
  • Holds both players' deposits
  • Is controlled by the server (private key server-side only)

Deposit Instructions

Both players see:

Wager accepted! Deposit required.
Send exactly $10 USDC to: 7xKX...AsU
Waiting for both deposits...

Deposit Verification

The server monitors the escrow wallet's USDC balance:

const escrowAta = getAssociatedTokenAddressSync(USDC_MINT, escrowPublicKey);
const account = await getAccount(connection, escrowAta);
const balance = account.amount;
 
if (balance >= expectedTotal) {
  // Both deposited - start battle!
}

Deposit Timeout

If both deposits aren't received within a timeout period:

  • Wager is cancelled
  • Any received deposits should be refunded
  • Players are notified

Battle Phase

Once both deposits are confirmed:

  1. Battle room created automatically
  2. Both players joined to the room
  3. Normal battle rules apply
  4. Timer active (turn timer, total timer)

The battle proceeds exactly like any Pokémon Showdown match. The wager doesn't affect gameplay.

Special Cases

Forfeit: If a player forfeits, they lose the wager.

Disconnect: If a player disconnects and doesn't return before timeout, they lose.

Timer loss: Running out of time = loss.

Draw: In the rare case of a draw (both last Pokémon faint simultaneously), special handling is needed (currently: investigate manually).


Settlement

When the battle ends, settlement happens automatically.

Winner Determination

The game server already determines winners - we hook into the existing onBattleEnd handler:

handlers: {
  onBattleEnd: async (battle, winnerid, players) => {
    // Find the wager for this battle
    const wager = getWager(battle.wagerMatchId);
    if (!wager) return;
    
    // Get winner's wallet
    const winnerWallet = getUserWallet(winnerid);
    
    // Settle!
    await settleWager(matchId, winnerWallet);
  }
}

Payout Calculation

const totalPot = wager.wagerAmountAtomic * 2n;  // Both deposits
const houseFee = (totalPot * BigInt(HOUSE_CUT_PERCENT)) / 100n;
const winnerPayout = totalPot - houseFee;

Example:

  • Wager: $10 each
  • Total pot: $20
  • House fee (5%): $1
  • Winner receives: $19

Settlement Transaction

The server builds and signs the transaction:

const instructions = [
  // Transfer winnings to winner
  createTransferCheckedInstruction(
    escrowAta,
    USDC_MINT,
    winnerAta,
    escrowKeypair.publicKey,
    winnerPayout,
    USDC_DECIMALS
  ),
  // Transfer house fee (if configured)
  createTransferCheckedInstruction(
    escrowAta,
    USDC_MINT,
    houseAta,
    escrowKeypair.publicKey,
    houseFee,
    USDC_DECIMALS
  )
];
 
const transaction = new VersionedTransaction(message);
transaction.sign([escrowKeypair]);
await connection.sendTransaction(transaction);

Player Notification

Winner:

🎉 You won the wager!
$19.00 USDC has been sent to your wallet.
TX: 5xYz...

Loser:

Better luck next time!
Gary won the $20.00 pot.

House Fee

The house fee is configurable via environment variable:

HOUSE_CUT_PERCENT=5  # 5% default

Where it goes:

  • Sent to HOUSE_WALLET_ADDRESS in the same settlement transaction
  • If not configured, entire pot goes to winner

Why a house fee?

  • Covers operational costs
  • Funds future development
  • Standard practice in competitive gaming

Facilitator Integration

After settlement, the wager is registered as an x402 payment event:

await fetch(`${FACILITATOR_URL}/api/facilitator/events`, {
  method: 'POST',
  body: JSON.stringify({
    events: [{
      type: 'payment',
      payTo: escrowPublicKey,
      network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
      asset: 'USDC',
      amount: totalPot.toString(),
      resourceUrl: `https://battle.pokedexter.gg/match/${matchId}`,
      txSignature: settlementTxSignature,
    }]
  })
});

This enables:

  • Tracking in Dexter marketplace stats
  • Future spectator betting features
  • Transaction history and auditing

Limits and Configuration

SettingDefaultEnvironment Variable
Minimum wager$1MIN_WAGER_USD
Maximum wager$100MAX_WAGER_USD
House fee5%HOUSE_CUT_PERCENT
Challenge timeout5 minHardcoded
Deposit timeoutTBDNot yet implemented

Security Considerations

Escrow Safety

  • Fresh keypair per match = isolated risk
  • Server-side keys = users can't steal from escrow
  • On-chain verification = transparent payouts

Throwing Prevention

  • Random Battle format = can't pre-select bad team
  • Timer rules = can't stall indefinitely
  • Future: Pattern detection for suspicious losses

Dispute Resolution

Currently manual. If something goes wrong:

  1. Check on-chain transaction history
  2. Verify battle outcome in logs
  3. Manual intervention if needed

Next Steps