Predictu
Wallet System

S2S Transaction Log

Every balance operation that Predictu sends to an operator’s S2S callback server is recorded in the S2S transaction log. This provides a complete audit trail of every cent that moves through the platform, enabling dispute resolution, operator reconciliation, and regulatory compliance.

Core invariant: Every S2S callback creates a transaction log entry. There is no way to trigger a balance operation without leaving a trace. The transaction log is append-only - entries are never updated or deleted.

Design Principles

  • Append-only log - The s2s_transactions table only supports inserts. To reverse a transaction, a new compensating entry is created (e.g., a BET_ROLLBACK to reverse a BET_MAKE).
  • Idempotency - Every transaction carries a unique request_id that serves as an idempotency key. If the operator receives the same request twice (e.g., due to retries), they return DUPLICATE_TRANSACTION without reprocessing.
  • Full context - Each log entry records the callback type, amount, operator response, HTTP status, response time, and any error details. This enables complete reconstruction of transaction history.
  • Operator reconciliation - The transaction log serves as Predictu’s side of the ledger. Operators can compare it against their own records to verify every transaction matches.

Transaction Log Schema

The s2s_transactions table is Predictu’s record of every balance operation sent to operators.

CREATE TABLE s2s_transactions (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  operator_id     UUID NOT NULL REFERENCES operators(id),
  user_id         UUID NOT NULL REFERENCES users(id),
  request_id      UUID NOT NULL UNIQUE,
  type            TEXT NOT NULL,
  amount          NUMERIC(12, 2) NOT NULL,
  balance_after   NUMERIC(12, 2),
  reference_id    TEXT,
  status          TEXT NOT NULL DEFAULT 'pending',
  response_code   TEXT,
  response_time   INTEGER,
  error_detail    TEXT,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Indexes for common queries
CREATE INDEX idx_s2s_tx_operator ON s2s_transactions(operator_id);
CREATE INDEX idx_s2s_tx_user ON s2s_transactions(user_id, created_at DESC);
CREATE INDEX idx_s2s_tx_request ON s2s_transactions(request_id);
CREATE INDEX idx_s2s_tx_type ON s2s_transactions(type);
CREATE INDEX idx_s2s_tx_reference ON s2s_transactions(reference_id);

Field Reference

FieldTypeDescription
idUUIDUnique identifier for this transaction log entry.
operator_idUUIDThe operator whose S2S endpoint was called. Foreign key to operators.
user_idUUIDThe user whose balance was affected. Foreign key to users.
request_idUUIDUnique idempotency key sent with the S2S callback.
typeTEXTS2S callback type (see transaction types below).
amountNUMERIC(12,2)The amount of the transaction in dollars. Positive for credits, negative for debits.
balance_afterNUMERIC(12,2)The user’s balance as reported by the operator after the transaction.
reference_idTEXTLinks to the trade, position, or settlement that caused this transaction.
statusTEXTTransaction status: pending, success, failed, or retrying.
response_codeTEXTThe operator’s response status code (OK, INSUFFICIENT_FUNDS, ERROR, etc.).
response_timeINTEGERRound-trip time in milliseconds for the S2S callback.
error_detailTEXTError message from the operator or network error detail.
created_atTIMESTAMPTZWhen the transaction was initiated. Auto-set by the database.

Transaction Types

Each transaction log entry is tagged with the S2S callback type that was sent to the operator. These types map directly to wallet adapter methods.

TypeDirectionDescription
BALANCEQueryBalance check. No funds moved - just a read of the player’s current balance.
BET_MAKEDebitPlayer purchased shares in a market. Operator deducts the cost.
BET_SELLCreditPlayer sold shares back. Operator credits the proceeds.
BET_WINCreditMarket resolved in the player’s favor. Operator credits the payout ($1 per share).
BET_LOSENotificationMarket resolved against the player. Informational - no funds moved.
BET_REFUNDCreditMarket voided. Operator refunds the player’s original cost basis.
BET_ROLLBACKCreditTrade failed after debit. Operator reverses the original BET_MAKE debit.

Callback Attempt Log

In addition to the transaction-level log, Predictu records every individual HTTP attempt in the s2s_callback_log table. A single transaction may have multiple callback attempts if retries are needed.

CREATE TABLE s2s_callback_log (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  transaction_id  UUID NOT NULL REFERENCES s2s_transactions(id),
  attempt         INTEGER NOT NULL DEFAULT 1,
  http_status     INTEGER,
  response_body   JSONB,
  response_time   INTEGER,
  error           TEXT,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_callback_log_tx ON s2s_callback_log(transaction_id);
Retry behavior: HTTP errors and ERROR status responses trigger exponential backoff retries (configurable max retries and base backoff). Business errors like INSUFFICIENT_FUNDS and PLAYER_NOT_FOUND are terminal and not retried. Each attempt is logged regardless of outcome.

Audit Trail

The append-only nature of the transaction log means that a complete financial history is always available. This is used for:

  • Operator reconciliation - Operators can request a transaction report for any time period and compare it against their own records. Every transaction includes the request_id that the operator also received.
  • Dispute resolution - When a player questions a balance, support can trace every S2S callback that affected it, including the operator’s response and response time.
  • Operator reporting - Operators can view all transactions for their players through the Operator Dashboard, filtered by type, date range, or player.
  • Regulatory compliance - The immutable transaction log satisfies audit requirements for financial record-keeping.

Common Audit Queries

-- Get full transaction history for a user
SELECT type, amount, balance_after, status, response_code, created_at
FROM s2s_transactions
WHERE user_id = :userId
ORDER BY created_at DESC;

-- Get all transactions for an operator in a date range
SELECT type, amount, user_id, status, created_at
FROM s2s_transactions
WHERE operator_id = :operatorId
  AND created_at BETWEEN :startDate AND :endDate
ORDER BY created_at DESC;

-- Find a transaction by idempotency key
SELECT *
FROM s2s_transactions
WHERE request_id = :requestId;

-- Daily transaction summary for an operator
SELECT
  DATE(created_at) as day,
  type,
  COUNT(*) as count,
  SUM(amount) as total
FROM s2s_transactions
WHERE operator_id = :operatorId AND status = 'success'
GROUP BY DATE(created_at), type
ORDER BY day DESC;

-- Failed transactions requiring attention
SELECT *
FROM s2s_transactions
WHERE status = 'failed'
  AND type IN ('BET_WIN', 'BET_REFUND', 'BET_ROLLBACK')
ORDER BY created_at DESC;

Worked Example

The following example shows a complete sequence of S2S transactions for a player who buys shares, sells some, and has a market resolve.

#TypeAmountBalance AfterStatus
1BALANCE$0.00$10,000.00success
2BET_MAKE-$32.50$9,967.50success
3BET_MAKE-$18.00$9,949.50success
4BET_SELL+$20.00$9,969.50success
5BET_WIN+$50.00$10,019.50success
6BET_LOSE$0.00$10,019.50success
Loss notifications: Entry #6 is a BET_LOSE notification with amount $0.00. Losses are recorded in the transaction log for audit completeness even though they don’t move funds. The reference_id links back to the position, so the loss can always be traced.

Integration with Wallet Adapter

The wallet adapter is the only component that writes to the S2S transaction log. Each adapter method creates a transaction log entry, sends the S2S callback to the operator, and updates the entry with the operator’s response.

// The flow for a buy trade:
//
// Trading Engine
//   └─> WalletAdapter.debitForBet(userId, $32.50, "tr_abc123")
//         └─> S2SWalletAdapter
//               ├─> INSERT into s2s_transactions (status: pending)
//               ├─> Build JWT-signed request
//               ├─> POST to operator callback URL
//               │     ├─> Type: BET_MAKE
//               │     ├─> Amount: 3250 (subunits)
//               │     └─> Request ID: uuid
//               ├─> Operator responds { status: "ok", balance: 996750 }
//               ├─> UPDATE s2s_transactions (status: success)
//               └─> Return { success: true, balance_after: 9967.50 }

The S2S transaction log is intentionally simple. It does not contain business logic about trading, risk, or settlement. It only records the fact that a balance operation was requested and what the operator responded. All business decisions happen in the layers above it.