Reconciliation

Nightly PSP sync. Detects and resolves discrepancies between our records and PSP data.

Phase 2
Spec
Reconciliation is a background job that runs every night and compares our transaction records with current statuses from the PSP. It exists for one reason: find payments we missed or misrecorded -- and fix them before the player contacts support. For a product manager it is a safety net. For a developer -- a cron job that calls getTransactionStatus() on suspicious transactions.
Reconciliation does not replace webhook handling. Webhooks remain the primary mechanism -- reconciliation only catches what fell through. If reconciliation regularly finds many discrepancies, that is a signal that webhook delivery is broken and should be fixed first.
1.

Why It Is Needed

Webhooks get lost

A PSP webhook can fail to deliver due to network issues, server restarts, or our endpoint being temporarily unavailable. Without reconciliation, a successful payment sits as TIMED_OUT forever.

TTL fires before blockchain confirms

A player sends crypto just as our TTL timer fires. Our system marks the transaction TIMED_OUT while the payment is actually on its way. Reconciliation detects this and credits the balance retroactively.

PSP status diverges from ours

Our state machine shows PROCESSING but the PSP has already settled the transaction as paid or rejected. This can happen after a cascade attempt where the first PSP actually processed the payment despite returning an error.

2.

Schedule & Configuration

03:00 UTC

Primary run

RECONCILIATION_CRON

Daily at 03:00 UTC -- low-traffic window. Processes all candidates from the previous 48 hours.

48 h

Lookback window

RECONCILIATION_LOOKBACK_HOURS

Transactions older than 48 hours are considered final and excluded. Prevents re-processing settled transactions.

10

Alert threshold

RECONCILIATION_ALERT_THRESHOLD

If more than 10 credits are applied in a single run, ops team is notified. Signals a systemic webhook delivery problem.

3.

Algorithm

1

Select candidate transactions

Query transactions table for rows in states that may need resolution: PROCESSING older than 30 min, TIMED_OUT within the last 48 hours, PENDING_CONFIRMATION older than 2 hours.

SELECT * FROM transactions
WHERE status IN ('PROCESSING', 'TIMED_OUT', 'PENDING_CONFIRMATION')
  AND updated_at < NOW() - INTERVAL '30 minutes'
  AND created_at > NOW() - INTERVAL '48 hours'
2

Call getTransactionStatus() for each

For each candidate, call the adapter's getTransactionStatus(psp_payment_id) to get the current status directly from the PSP. Adapter returns a UnifiedStatus.

3

Compare PSP status with our status

If statuses match -- no action, log a reconciliation_check event. If they differ -- discrepancy detected, proceed to resolution.

4

PSP = COMPLETED, ours = TIMED_OUT / PROCESSING

Credit

Payment was successful but we missed it. Transition transaction to COMPLETED via state machine, credit player balance via Wallet Engine. Log reconciliation_credit event.

5

PSP = FAILED / EXPIRED, ours = PROCESSING / TIMED_OUT

Failure

Payment definitively failed. Transition transaction to FAILED. No balance change. Log reconciliation_failed event. If a withdrawal hold was active -- release it back to available.

6

PSP status still pending

PSP also shows PROCESSING or equivalent. Leave our status unchanged. Schedule for re-check in the next reconciliation run.

7

Generate reconciliation report

After processing all candidates, write a summary to reconciliation_reports: total checked, credits applied, failures resolved, errors encountered. Alert ops team if credit count exceeds threshold.

4.

Discrepancy Types

Our statusPSP statusActionRisk
TIMED_OUTCOMPLETEDCredit balance, transition to COMPLETED
High
PROCESSINGCOMPLETEDCredit balance, transition to COMPLETED
High
PROCESSINGFAILEDTransition to FAILED, no balance change
Medium
TIMED_OUTFAILEDConfirm FAILED state, log only
Low
COMPLETEDFAILEDCritical alert to ops -- do not auto-resolve, manual review required
High
PROCESSINGNot foundAlert ops -- psp_payment_id mismatch or PSP data loss
High

The COMPLETED → FAILED scenario is never auto-resolved. The balance was already credited -- automatic debit means a direct financial loss to the player without their knowledge. Manual ops review only.

5.

Events & Reporting

event_typeWhenpayload
reconciliation_checkEach transaction checked, statuses match{ psp_status, our_status, checked_at }
reconciliation_creditMissed payment found, balance credited{ psp_status, amount_credited, wallet_tx_id }
reconciliation_failedTransaction resolved as FAILED{ psp_status, previous_status }
reconciliation_alertCritical discrepancy, manual review needed{ our_status, psp_status, reason }
reconciliation_runEnd of each reconciliation job run{ total_checked, credits, failures, alerts, duration_ms }

reconciliation_reports table (separate from transaction_events)

iduuidPrimary key
run_attimestamptzWhen the job ran
total_checkedintTotal transactions inspected
credits_appliedintTransactions where balance was credited
failures_closedintTransactions resolved as FAILED
alerts_raisedintCritical discrepancies requiring manual review
duration_msintTotal run time in milliseconds
6.

Fee & Revenue Model

Every transaction carries two cost layers: the PSP fee (we pay the provider) and the platform markup (we charge the operator). Reconciliation is the single place where both layers are brought together -- the PSP invoices based on actual transactions, and the platform invoices the operator from the same data. The difference between these two numbers is platform revenue.

PSP Fee Structure

Percentage fee

Applied as % of settlement_amount. Typical range: 1.2%--3.5% for cards, 0.5%--1.5% for crypto. Varies by PSP contract and monthly volume tier.

Flat fee per transaction

Fixed amount regardless of transaction size. Typical: $0.10--$0.30 per transaction. Often combined with percentage fee (e.g. 1.5% + $0.20).

Decline & chargeback fees

PSPs charge per declined transaction ($0.05--$0.15) and per chargeback dispute ($15--$100). These appear as separate line items on the PSP invoice and must be tracked separately.

Platform Revenue Models

Markup on PSP fee

Recommended

Platform charges operator PSP fee + markup percentage. Example: PSP charges 1.8%, platform charges operator 2.3%, pocket 0.5%. Simplest model -- one number per transaction. Stored as platform_fee_pct in psp_configs.

Flat fee per transaction

Platform charges operator a fixed amount per transaction regardless of PSP cost. Example: $0.50 per deposit processed. Predictable for operator, less upside for platform on large transactions. Stored as platform_fee_flat in psp_configs.

Revenue share

Platform takes a percentage of the operator's GGR (Gross Gaming Revenue) attributable to payment processing. More complex to calculate -- requires game revenue data. Typically used for strategic large-volume operators. Defined in operator contract, not in payment config.

FX spread revenue

When operators support multiple display currencies, the platform applies a configurable spread (e.g. 1.5%) on top of the mid-market rate (see FX / Multi-currency). This spread is pure platform revenue -- no PSP cost involved. Tracked as fx_spread_revenue in the fee ledger.

Table transaction_fees -- fee ledger

A separate transaction_fees table stores the fee breakdown for every transaction. This allows the reconciliation job to compare the actual PSP invoice against what the platform expected -- and detect discrepancies before the invoice arrives.
ColumnTypeDescription
id
uuid PK
Fee record ID
transaction_id
uuid FK
Reference to the transaction in transactions table
operator_id
uuid FK
Operator being charged
psp_id
uuid FK
PSP that processed the transaction
psp_fee_pct
numeric(6,4)
PSP percentage fee applied (e.g. 0.018 = 1.8%)
psp_fee_flat
numeric(10,4)
PSP flat fee per transaction in settlement currency
psp_fee_total
numeric(14,4)
Total PSP cost: (settlement_amount * psp_fee_pct) + psp_fee_flat
platform_fee_pct
numeric(6,4)
Platform markup percentage charged to operator
platform_fee_flat
numeric(10,4)
Platform flat fee per transaction charged to operator
platform_fee_total
numeric(14,4)
Total operator charge: (settlement_amount * platform_fee_pct) + platform_fee_flat
fx_spread_revenue
numeric(14,4)
Revenue from FX spread on this transaction (0 if no currency conversion)
platform_revenue
numeric(14,4)
Computed: platform_fee_total - psp_fee_total + fx_spread_revenue
psp_invoiced_amount
numeric(14,4)
Actual fee from PSP invoice -- populated by reconciliation job, null until invoice arrives
fee_discrepancy
numeric(14,4)
psp_invoiced_amount - psp_fee_total -- non-zero means unexpected fee from PSP
currency
char(3)
All amounts in this row are in this currency (settlement currency)
created_at
timestamptz
When this fee record was created (at transaction completion)

Settlement Report

Per-operator monthly report

Reconciliation job generates a monthly settlement report per operator: total transaction volume, total PSP fees paid, total platform fees charged to operator, net revenue, FX spread revenue, and any chargeback costs attributed to the operator. Stored in operator_settlement_reports table.

PSP invoice matching

Each PSP sends a monthly invoice. The reconciliation job ingests the invoice line items and matches them against transaction_fees.psp_invoiced_amount. Any line item that differs from the expected fee by more than $0.01 is flagged as fee_discrepancy and sent to ops for review.

Revenue dashboard inputs

Admin Panel revenue dashboard reads from transaction_fees -- aggregated by operator, PSP, method, and time period. Metrics: gross payment volume, PSP cost ratio, platform revenue, effective take rate (platform_revenue / settlement_amount). All in settlement currency.

DEPO44 | RECONCILIATION SPEC v1 | PHASE 2