Predictu
S2S Protocol

Callback Methods

This page documents every S2S callback method. Each method includes the request and response JSON, field descriptions, and behavioral notes. Your callback endpoint must handle all eight methods.

Reminder: All monetary amounts are in subunits (cents).$1.00 = 100. All requests are POST to your callback_url with a JWT in the Authorization header.

PING

Health check callback. Predictu sends this during operator onboarding, periodic monitoring, and when an admin clicks "Test Connection" in the God Mode dashboard. No player context - just verify connectivity and JWT validation.

Request

{
  "method": "PING",
  "timestamp": "2026-03-19T14:30:00.000Z",
  "request_id": "d4e5f6a7-b8c9-0123-def0-456789abcdef",
  "operator_id": "op_abc123",
  "params": {}
}

Response

{
  "status": "OK"
}

The params object is intentionally empty. Your handler should verify the JWT signature, confirm the operator_id is recognized, and return OK. No balance or transaction ID is needed.

Tip: Use PING responses to verify your JWT verification is working correctly before testing with real wallet operations.

BALANCE

Fetch a player's current wallet balance. Called before every trade to validate sufficient funds, and whenever the UI needs to display an up-to-date balance. This callback has no side effects - it is a read-only query.

Request

{
  "method": "BALANCE",
  "timestamp": "2026-03-19T14:30:00.000Z",
  "request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "operator_id": "op_abc123",
  "params": {
    "player_id": "player_456",
    "currency": "USD"
  }
}

Params

FieldTypeDescription
player_idstringThe operator's external player identifier. This is the external_user_id from the embed initialization.
currencystringCurrency code. Currently always "USD".

Response

{
  "status": "OK",
  "balance": 1000000
}

Return the player's current balance in subunits. If the player_id is not found, you may either return PLAYER_NOT_FOUND or auto-create the player with a default balance (the reference implementation auto-creates with 10,000.00 = 1,000,000 subunits).

BET_MAKE

Debit a player's wallet for a new prediction market bet. This is the most critical callback - it must be atomic and idempotent. If the debit succeeds, Predictu records the trade and updates the player's position. If the debit fails, no trade is recorded.

Request

{
  "method": "BET_MAKE",
  "timestamp": "2026-03-19T14:30:00.000Z",
  "request_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "operator_id": "op_abc123",
  "params": {
    "player_id": "player_456",
    "transaction_id": "tx_pred_001",
    "amount": 5200,
    "currency": "USD",
    "bet": {
      "market_id": "mkt_btc_100k",
      "market_title": "Will Bitcoin exceed $100k by June 2026?",
      "outcome": "yes",
      "odds": 1.92,
      "shares": 100,
      "execution_price": 52
    }
  }
}

Params

FieldTypeDescription
player_idstringThe operator's external player identifier.
transaction_idstringPredictu's unique identifier for this specific transaction. Different from request_id (the idempotency key).
amountnumberAmount to debit in subunits. Example: 5200 = $52.00.
currencystringCurrency code. Currently always "USD".

Bet Object

The bet object provides full context about the prediction market bet. This is informational for the operator's records and reporting - the debit amount is what matters for the wallet operation.

FieldTypeDescription
market_idstringUnique market identifier in Predictu's system.
market_titlestringHuman-readable market question. Example: "Will Bitcoin exceed $100k by June 2026?"
outcome"yes" | "no"Which outcome the player is betting on.
oddsnumberDecimal odds at time of execution. Example: 1.92 means a $52 bet pays $100 if correct.
sharesnumberNumber of shares purchased. Each share pays $1.00 (100 subunits) if the outcome is correct.
execution_pricenumberPrice per share in cents (0–100). This is the spread-adjusted price, not the mid price. Example: 52 means 52c per share.

Response

// Success
{
  "status": "OK",
  "balance": 948000,
  "transaction_id": "op-tx-bet-001"
}

// Insufficient funds
{
  "status": "INSUFFICIENT_FUNDS",
  "balance": 3200,
  "error_message": "Player balance 3200 < debit amount 5200"
}

// Player not found
{
  "status": "PLAYER_NOT_FOUND",
  "error_message": "No player with ID player_456"
}
Idempotency: If you receive a BET_MAKE with a request_id you have already successfully processed, return DUPLICATE_TRANSACTION with the cached balance and transaction ID. Do not debit the player again.

BET_WIN

Credit a player's wallet after their bet wins. Sent when a market resolves and the player's chosen outcome is correct. The credit amount equals shares × 100subunits ($1.00 per share).

Request

{
  "method": "BET_WIN",
  "timestamp": "2026-03-19T14:30:00.000Z",
  "request_id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
  "operator_id": "op_abc123",
  "params": {
    "player_id": "player_456",
    "transaction_id": "tx_pred_002",
    "parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "amount": 10000,
    "currency": "USD",
    "settlement": {
      "market_id": "mkt_btc_100k",
      "market_title": "Will Bitcoin exceed $100k by June 2026?",
      "winning_outcome": "yes",
      "shares": 100,
      "cost_basis": 5200,
      "profit": 4800
    }
  }
}

Params

FieldTypeDescription
player_idstringThe operator's external player identifier.
transaction_idstringPredictu's unique transaction ID for this settlement credit.
parent_transaction_idstringThe request_id of the original BET_MAKE callback. Links this settlement to the originating bet.
amountnumberAmount to credit in subunits. Equal to shares × 100. Example: 100 shares = 10,000 subunits ($100.00).
currencystringCurrency code.

Settlement Object

FieldTypeDescription
market_idstringMarket that was resolved.
market_titlestringHuman-readable market question.
winning_outcome"yes" | "no"The outcome that won.
sharesnumberNumber of shares the player held.
cost_basisnumberWhat the player originally paid for these shares, in subunits.
profitnumberNet profit in subunits. Equal to amount - cost_basis. Example: 10000 - 5200 = 4800 ($48.00 profit).

Response

{
  "status": "OK",
  "balance": 1048000,
  "transaction_id": "op-tx-win-001"
}

BET_LOST

Notification that a player's bet has lost after market resolution. This callback has no money movement - the amount is always 0. It exists purely for bookkeeping so the operator can close the bet in their records and update reporting.

Request

{
  "method": "BET_LOST",
  "timestamp": "2026-03-19T14:30:00.000Z",
  "request_id": "d4e5f6a7-b8c9-0123-def0-456789abcdef",
  "operator_id": "op_abc123",
  "params": {
    "player_id": "player_456",
    "transaction_id": "tx_pred_003",
    "parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "amount": 0,
    "currency": "USD",
    "settlement": {
      "market_id": "mkt_btc_100k",
      "market_title": "Will Bitcoin exceed $100k by June 2026?",
      "winning_outcome": "no",
      "player_outcome": "yes",
      "shares": 100,
      "cost_basis": 5200
    }
  }
}

Settlement Object

FieldTypeDescription
market_idstringMarket that was resolved.
market_titlestringHuman-readable market question.
winning_outcome"yes" | "no"The outcome that won.
player_outcome"yes" | "no"The outcome the player bet on (which lost).
sharesnumberNumber of shares the player held (now worthless).
cost_basisnumberWhat the player originally paid, in subunits. This is the player's total loss.

Response

{
  "status": "OK",
  "balance": 948000,
  "transaction_id": "op-tx-loss-001"
}
No wallet change needed. The player's balance was already debited duringBET_MAKE. Just record the loss in your transaction log and return the current balance.

BET_REFUND

Fully refund a player's bet. Triggered when a market is voided, cancelled by an admin, or determined to be invalid. The refund amount equals the original BET_MAKEdebit - the player is made completely whole.

Request

{
  "method": "BET_REFUND",
  "timestamp": "2026-03-19T14:30:00.000Z",
  "request_id": "e5f6a7b8-c9d0-1234-ef01-567890abcdef",
  "operator_id": "op_abc123",
  "params": {
    "player_id": "player_456",
    "transaction_id": "tx_pred_004",
    "parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "amount": 5200,
    "currency": "USD",
    "reason": "Market voided: ambiguous resolution criteria"
  }
}

Params

FieldTypeDescription
player_idstringThe operator's external player identifier.
transaction_idstringPredictu's unique transaction ID for this refund.
parent_transaction_idstringThe request_id of the original BET_MAKE being refunded.
amountnumberFull cost basis to credit back, in subunits. Equal to the original BET_MAKE amount.
currencystringCurrency code.
reasonstringHuman-readable reason for the refund. Useful for audit trails and customer support.

Response

{
  "status": "OK",
  "balance": 1000000,
  "transaction_id": "op-tx-refund-001"
}

BET_ROLLBACK

Reverse a BET_MAKE debit that succeeded at the operator but failed on Predictu's side (e.g., the trade recording or position update failed after the debit). This is the compensating transaction that ensures atomicity across the distributed system.

Critical: If you receive a BET_ROLLBACK, it means the debit was successful but the trade could not be completed. You must credit the amount back. Failing to process a rollback leaves the player's balance permanently incorrect.

Request

{
  "method": "BET_ROLLBACK",
  "timestamp": "2026-03-19T14:30:01.000Z",
  "request_id": "f6a7b8c9-d0e1-2345-f012-678901abcdef",
  "operator_id": "op_abc123",
  "params": {
    "player_id": "player_456",
    "transaction_id": "tx_pred_005",
    "parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "amount": 5200,
    "currency": "USD",
    "reason": "Buy failed after debit - Failed to record trade: unique constraint violation"
  }
}

Params

FieldTypeDescription
player_idstringThe operator's external player identifier.
transaction_idstringPredictu's unique transaction ID for this rollback.
parent_transaction_idstringThe request_id of the BET_MAKE being reversed.
amountnumberAmount to credit back, in subunits.
currencystringCurrency code.
reasonstringTechnical reason for the rollback. Includes the downstream error message.

Response

{
  "status": "OK",
  "balance": 1000000,
  "transaction_id": "op-tx-rollback-001"
}

BET_SELL

Credit a player for selling a position before market resolution. Unlike BET_WIN, the credit amount is based on the current market price (minus spread), not the full share value. The player may realize a profit or a loss depending on price movement.

Request

{
  "method": "BET_SELL",
  "timestamp": "2026-03-19T14:30:00.000Z",
  "request_id": "a7b8c9d0-e1f2-3456-0123-789012abcdef",
  "operator_id": "op_abc123",
  "params": {
    "player_id": "player_456",
    "transaction_id": "tx_pred_006",
    "parent_transaction_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "amount": 6340,
    "currency": "USD",
    "sale": {
      "market_id": "mkt_btc_100k",
      "market_title": "Will Bitcoin exceed $100k by June 2026?",
      "outcome": "yes",
      "shares_sold": 100,
      "execution_price": 65,
      "realized_pnl": 1140
    }
  }
}

Sale Object

FieldTypeDescription
market_idstringMarket being sold from.
market_titlestringHuman-readable market question.
outcome"yes" | "no"The outcome the player held and is selling.
shares_soldnumberNumber of shares being sold.
execution_pricenumberSell price per share in cents (0–100). This is the bid price (mid minus spread).
realized_pnlnumberRealized profit/loss in subunits. Positive = profit, negative = loss. Calculated as (execution_price - avg_buy_price) × shares_sold.

Response

{
  "status": "OK",
  "balance": 1006340,
  "transaction_id": "op-tx-sell-001"
}

P&L Examples

ScenarioBuy PriceSell PriceSharesCostProceedsRealized P&L
Price went up52c65c100$52.00$63.40 *+$11.40
Price went down52c38c100$52.00$36.48 *-$15.52
Break even52c52c100$52.00$50.96 *-$1.04

* Sell proceeds include spread. Even at the same mid price, the sell price (bid) is lower than the buy price (ask) due to spread, so the player realizes a small loss on an immediate round-trip.

Idempotency & Duplicate Handling

Every S2S request includes a unique request_id (UUID v4). This serves as the idempotency key. Your callback handler must implement duplicate detection.

Implementation Flow

1
Receive callback - Extract request_id from the request body.
2
Check for duplicate - Look up request_id in your transaction table (e.g., SELECT * FROM transactions WHERE predictu_request_id = ?).
3
If duplicate found - Return DUPLICATE_TRANSACTION with the player's current balance and the cached transaction_id. Do not re-process the wallet operation.
4
If not a duplicate - Process the wallet operation normally, record the transaction with the request_id, and return OK.
// Idempotency check (reference implementation)
const existingTx = findTransactionByRequestId(request_id);
if (existingTx) {
  console.log("Duplicate request_id, returning cached result");
  const player = getPlayer(params.player_id);
  return {
    status: "DUPLICATE_TRANSACTION",
    balance: player.balance,
    transaction_id: existingTx.id,
  };
}
Why duplicates happen: Predictu retries failed requests with the samerequest_id. If the first attempt succeeded but Predictu didn't receive the response (e.g., network timeout), the retry will carry the same request_id. Without idempotency, the player would be double-debited.

Error Handling Best Practices

Operator Side

  • Always return valid JSON, even for errors. Predictu parses the response as JSON.
  • Use the correct status codes: INSUFFICIENT_FUNDS for balance issues, PLAYER_NOT_FOUND for missing players, ERROR for everything else.
  • Include balance in every response when possible, even error responses. This helps Predictu keep the UI in sync.
  • Log every callback with the request_id for troubleshooting.
  • Implement a transaction table with predictu_request_id as a unique index for idempotency.

Predictu Side

  • OK and DUPLICATE_TRANSACTION are treated as success - the trade proceeds.
  • INSUFFICIENT_FUNDS and PLAYER_NOT_FOUND are non-retryable business errors.
  • ERROR status and HTTP failures (timeouts, 5xx) trigger exponential backoff retries.
  • Non-JSON responses are treated as ERROR.
  • After all retry attempts are exhausted, the transaction is marked as failed.

Method Summary Reference

MethodWallet OpHas ParentHas Context ObjectRetryable
PINGNoneNoNoYes
BALANCEReadNoNoYes
BET_MAKEDebitNobetYes *
BET_WINCreditYessettlementYes
BET_LOSTNoneYessettlementYes
BET_REFUNDCreditYesNo (reason string)Yes
BET_ROLLBACKCreditYesNo (reason string)Yes
BET_SELLCreditYessaleYes

* BET_MAKE retries use the same request_id, so idempotent duplicate detection prevents double-debits.