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.
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.
| Field | Type | Description |
|---|---|---|
s2s_callback_url | string | The operator’s callback endpoint for all wallet operations |
s2s_secret | string | Shared secret for JWT signing |
currency_subunits | boolean | If 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
| Method | S2S Callback Type | Direction |
|---|---|---|
getBalance | BALANCE | Query operator for current balance |
debitForBet | BET_MAKE | Request debit from operator wallet |
creditForWin | BET_WIN | Request credit to operator wallet |
notifyLoss | BET_LOSE | Notify operator of loss (informational) |
creditForSell | BET_SELL | Request credit for position sell |
refund | BET_REFUND | Request refund for voided market |
rollback | BET_ROLLBACK | Request 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 Amount | currency_subunits = false | currency_subunits = true |
|---|---|---|
| $10.00 | Sent as 10 | Sent as 1000 |
| $0.65 | Sent as 0.65 | Sent as 65 |
| $1,000.00 | Sent as 1000 | Sent as 100000 |
Math.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
toSubunits($32.50)returns 3250 for subunit operators, or 32.50 for non-subunit.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
operator_id.getWalletAdapter(operatorId)returns an adapter instance configured for that operator’s S2S endpoint.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.
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.
