Predictu
Wallet System

Wallet Adapter

The wallet adapter routes all balance operations through the operator’s S2S callback server. It is the abstraction layer between Predictu’s trading engine and the operator’s wallet system, sending JWT-signed HTTP requests for every debit, credit, and balance check.

Why this matters: The casino operator is the house and owns all player balances. Predictu never holds player funds or takes counterparty risk. The trading engine, risk engine, and resolution engine all call the same wallet adapter interface. They never need to know the details of the operator’s wallet implementation. This decoupling is what makes the B2B integration possible.

Operator Configuration

Each operator registers their S2S callback details during onboarding. The wallet adapter reads this configuration to know where and how to send balance requests.

FieldTypeDescription
s2s_callback_urlstringThe operator’s callback endpoint for all wallet operations
s2s_secretstringShared secret for JWT signing
currency_subunitsbooleanIf true, S2S amounts are multiplied by 100 (cents)

Adapter Interface

The wallet adapter implements the WalletAdapter interface. This is the contract that the rest of the system depends on. Every method maps to an S2S callback type sent to the operator’s server.

interface WalletAdapter {
  /** Get the user's current balance */
  getBalance(userId: string): Promise<number>;

  /** Debit user's balance for a new bet */
  debitForBet(
    userId: string,
    amount: number,
    tradeId: string
  ): Promise<{ success: boolean; balance_after: number }>;

  /** Credit user's balance when they win a resolved market */
  creditForWin(
    userId: string,
    amount: number,
    positionId: string
  ): Promise<{ success: boolean; balance_after: number }>;

  /** Record/notify a loss when a market resolves against the user */
  notifyLoss(
    userId: string,
    costBasis: number,
    positionId: string
  ): Promise<void>;

  /** Credit user's balance when they sell a position */
  creditForSell(
    userId: string,
    amount: number,
    tradeId: string
  ): Promise<{ success: boolean; balance_after: number }>;

  /** Refund user's cost basis when a market is voided */
  refund(
    userId: string,
    amount: number,
    positionId: string
  ): Promise<{ success: boolean; balance_after: number }>;

  /** Rollback a failed trade (reverse a debit) */
  rollback(
    userId: string,
    amount: number,
    originalTradeId: string
  ): Promise<{ success: boolean; balance_after: number }>;
}

S2S Wallet Adapter

The operator runs their own wallet server. Predictu sends signed callbacks for every balance operation. This gives operators full control over their player funds. Instead of modifying a local database, the S2S adapter sends JWT-signed HTTP requests to the operator’s callback URL and interprets the response.

Method to Callback Mapping

MethodS2S Callback TypeDirection
getBalanceBALANCEQuery operator for current balance
debitForBetBET_MAKERequest debit from operator wallet
creditForWinBET_WINRequest credit to operator wallet
notifyLossBET_LOSENotify operator of loss (informational)
creditForSellBET_SELLRequest credit for position sell
refundBET_REFUNDRequest refund for voided market
rollbackBET_ROLLBACKRequest reversal of failed debit

Implementation

class S2SWalletAdapter implements WalletAdapter {
  private operator: OperatorConfig;

  constructor(operator: OperatorConfig) {
    this.operator = operator;
  }

  async getBalance(userId: string): Promise<number> {
    const response = await s2sDispatcher.send({
      operator: this.operator,
      type: "BALANCE",
      payload: { user_id: userId },
    });
    return this.fromSubunits(response.balance);
  }

  async debitForBet(
    userId: string,
    amount: number,
    tradeId: string
  ) {
    const response = await s2sDispatcher.send({
      operator: this.operator,
      type: "BET_MAKE",
      payload: {
        user_id: userId,
        amount: this.toSubunits(amount),
        transaction_id: tradeId,
      },
    });
    return {
      success: response.status === "ok",
      balance_after: this.fromSubunits(response.balance),
    };
  }

  async creditForWin(
    userId: string,
    amount: number,
    positionId: string
  ) {
    const response = await s2sDispatcher.send({
      operator: this.operator,
      type: "BET_WIN",
      payload: {
        user_id: userId,
        amount: this.toSubunits(amount),
        transaction_id: positionId,
      },
    });
    return {
      success: response.status === "ok",
      balance_after: this.fromSubunits(response.balance),
    };
  }

  // ... other methods follow the same pattern

  /** Convert dollars to subunits (cents) if operator uses subunits */
  private toSubunits(amount: number): number {
    return this.operator.currency_subunits
      ? Math.round(amount * 100)
      : amount;
  }

  /** Convert subunits (cents) back to dollars */
  private fromSubunits(amount: number): number {
    return this.operator.currency_subunits
      ? amount / 100
      : amount;
  }
}

Subunit Conversion

Many casino operators work in subunits (cents) rather than whole currency units (dollars). The S2S adapter handles this transparently using the currency_subunits flag on the operator config.

Conversion Examples

Internal Amountcurrency_subunits = falsecurrency_subunits = true
$10.00Sent as 10Sent as 1000
$0.65Sent as 0.65Sent as 65
$1,000.00Sent as 1000Sent as 100000
Rounding: When converting to subunits, the adapter usesMath.round(amount * 100) to avoid floating-point precision issues. When converting back, it uses simple division. All internal calculations in Predictu use dollar amounts with standard floating-point precision.

Conversion Flow

1
Trade engine calculates cost - e.g., 50 shares at $0.65 = $32.50 (internal dollar amount).
2
Adapter converts outbound - toSubunits($32.50)returns 3250 for subunit operators, or 32.50 for non-subunit.
3
Operator processes transaction - Operator sees the amount in their native format and processes accordingly.
4
Adapter converts inbound - Operator responds with balance in their format. fromSubunits() converts it back to dollars for Predictu.

How the Adapter is Initialized

The adapter is initialized at the point of use, not at startup. The system resolves the operator’s S2S configuration for each request.

Initialization Flow

1
Trade request arrives - The request includes the user ID.
2
Load user record - The user record includes operator_id.
3
Load operator config - Load the operator’s S2S configuration including callback URL, signing secret, and subunit preference.
4
Instantiate adapter - getWalletAdapter(operatorId)returns an adapter instance configured for that operator’s S2S endpoint.
5
Execute operation - The trading engine calls adapter methods. Every call results in a signed HTTP callback to the operator’s server.
Performance note: Operator configs are cached in memory with a 60-second TTL. The adapter does not hit the database on every trade. The adapter instance itself is lightweight and stateless (except for the operator config reference).

Error Handling

The wallet adapter handles errors from the operator’s S2S callback server.

S2S Errors

  • Operator returns error - e.g., insufficient funds, user not found. The adapter returns { success: false } with the error detail.
  • Network timeout - The S2S dispatcher retries with exponential backoff (1s, 2s, 4s, 8s, 16s). After 5 failures, the adapter throws a timeout error.
  • Invalid response - If the operator returns an unparseable response, the adapter treats it as a failure and the trade is rolled back.
Rollback guarantee: If a debitForBet succeeds but the subsequent trade execution fails, the trading engine always calls rollback()to reverse the debit. This is critical because the operator has already deducted the funds on their side.

The wallet adapter pattern is inspired by the Adapter design pattern from the Gang of Four. It cleanly separates Predictu’s trading logic from the operator’s wallet implementation, so the trading engine never needs to know the details of how the operator manages balances.