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.
Design Principles
- Append-only log - The
s2s_transactionstable only supports inserts. To reverse a transaction, a new compensating entry is created (e.g., aBET_ROLLBACKto reverse aBET_MAKE). - Idempotency - Every transaction carries a unique
request_idthat serves as an idempotency key. If the operator receives the same request twice (e.g., due to retries), they returnDUPLICATE_TRANSACTIONwithout 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
| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier for this transaction log entry. |
operator_id | UUID | The operator whose S2S endpoint was called. Foreign key to operators. |
user_id | UUID | The user whose balance was affected. Foreign key to users. |
request_id | UUID | Unique idempotency key sent with the S2S callback. |
type | TEXT | S2S callback type (see transaction types below). |
amount | NUMERIC(12,2) | The amount of the transaction in dollars. Positive for credits, negative for debits. |
balance_after | NUMERIC(12,2) | The user’s balance as reported by the operator after the transaction. |
reference_id | TEXT | Links to the trade, position, or settlement that caused this transaction. |
status | TEXT | Transaction status: pending, success, failed, or retrying. |
response_code | TEXT | The operator’s response status code (OK, INSUFFICIENT_FUNDS, ERROR, etc.). |
response_time | INTEGER | Round-trip time in milliseconds for the S2S callback. |
error_detail | TEXT | Error message from the operator or network error detail. |
created_at | TIMESTAMPTZ | When 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.
| Type | Direction | Description |
|---|---|---|
BALANCE | Query | Balance check. No funds moved - just a read of the player’s current balance. |
BET_MAKE | Debit | Player purchased shares in a market. Operator deducts the cost. |
BET_SELL | Credit | Player sold shares back. Operator credits the proceeds. |
BET_WIN | Credit | Market resolved in the player’s favor. Operator credits the payout ($1 per share). |
BET_LOSE | Notification | Market resolved against the player. Informational - no funds moved. |
BET_REFUND | Credit | Market voided. Operator refunds the player’s original cost basis. |
BET_ROLLBACK | Credit | Trade 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);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_idthat 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.
| # | Type | Amount | Balance After | Status |
|---|---|---|---|---|
| 1 | BALANCE | $0.00 | $10,000.00 | success |
| 2 | BET_MAKE | -$32.50 | $9,967.50 | success |
| 3 | BET_MAKE | -$18.00 | $9,949.50 | success |
| 4 | BET_SELL | +$20.00 | $9,969.50 | success |
| 5 | BET_WIN | +$50.00 | $10,019.50 | success |
| 6 | BET_LOSE | $0.00 | $10,019.50 | success |
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.
