Find the money that lost its other half
Matching by amount and time can attach a transaction to the wrong payment, while calendar cut-offs make a legitimate delay look like lost money.
7 min read
When a payment system says money is missing, do not begin with the balance. Begin with the record that should have a counterpart and does not. Every monetary event makes a promise: another account will move, another lifecycle event will arrive, or an external statement will confirm the movement. The investigative job is to turn that promise into an invariant, then search for the first transaction that breaks it.
The counterpart is not always the debit sitting beside a credit. It may be a capture following an authorization, the line items composing a settlement batch, or the bank credit confirming a payout. Demand the pair too early and normal delay looks like loss. Compare only aggregate balances and the orphan disappears into the total. Reconciliation works in the middle: identity by identity, state by state, within an explicit time window.
Draw the crime scene as a fund flow
Double-entry bookkeeping gives the investigation its simplest law: a transaction records equal value on at least two accounts, once as a debit and once as a credit.1 In software, that becomes a write-time constraint. A journal transaction should not commit unless its signed postings sum to zero for each currency.
That law catches a torn internal write. It cannot prove that a processor captured the payment, that a settlement file arrived, or that a bank credited a payout. Those facts live in other systems. Stripe's public engineering account of its Ledger describes the larger abstraction as connected fund flows across system boundaries, measured for clearing, timeliness and completeness.2 The useful shift is from “are our books balanced?” to “did every expected handoff happen correctly and on time?”
Draw one path before opening a dashboard:
payment request → authorization → capture → settlement item → payout batch → bank credit
For every arrow, define four things: the identity that crosses the boundary, the permitted state transition, the amount equation, and the deadline. The result is a counterpart contract. A captured payment may require one settlement item in the same currency within the rail's expected reporting window. A payout may require many settlement items whose net sum equals one bank credit. “Other half” now has a precise meaning.
Do not arrest a payment for being late
Payment state is not decoration. Authorization reserves funds; capture moves the reserved funds into the next stage. Some methods separate those steps, allow partial capture, or let an uncaptured authorization expire.3 An authorized record with no capture is therefore not automatically broken. It may be legitimately pending, cancelled or expired.
This is why a single Boolean called reconciled is too blunt. Model the expected resolution. TigerBeetle's public two-phase transfer design, for example, distinguishes pending value from posted value and resolves a pending transfer by posting, voiding or expiry. A partial post releases the remainder, and resolution is represented by a new immutable transfer linked to the pending one.4 The specific implementation is less important than the discipline: every non-final state has a finite set of legal exits.
Time needs the same precision. Adyen's reporting documentation distinguishes the booking date, when an event such as capture occurs, from the value date, when funds are credited or debited and become available. It also documents settlement delays between the sales day and that value date.5 A join on calendar date will manufacture mismatches near cut-offs. Store event time, booking time, value time and ingestion time separately. Age an exception against the clock promised by its transition, not against whichever timestamp happens to be easiest to query.
An alert should consequently say “capture has no settlement item after its allowed window,” not “payment is old.” The former names a broken contract. The latter starts a scavenger hunt.
Follow identity before amount
Two payments can share an amount, currency and minute. That makes value-and-time matching useful as evidence, but dangerous as identity. Preserve the references emitted at each boundary and link them instead of overwriting one with the next: internal operation ID, authorization ID, capture or modification ID, settlement-line ID, payout-batch ID and bank trace reference.8
Payments standards make this principle explicit. ISO 20022 defines an EndToEndIdentification assigned by the initiating party, passed unchanged through the chain, and usable for reconciliation or for linking tasks related to the transaction. It separately defines transaction and clearing-system references for other scopes.6 Swift's UETR applies the same idea to cross-border tracking: one unique reference follows the payment through the chain and exposes status changes.7
The practical data structure is a graph, not a column named provider_reference. One authorization can lead to several captures.3 One capture can produce fee and net-settlement lines, while many settled transactions can feed one payout.8 A refund should point back to the economic event it reverses while retaining its own identity. When an upstream system changes its reference, add an edge; do not destroy the earlier address.
With that graph, the search becomes mechanical. Start from the complaint's known ID. Traverse outward until an expected edge is absent. If the capture exists but no settlement line references it, the break is between capture and settlement ingestion. If the line is present but not assigned to the payout, inspect batching. If the payout exists internally but the bank trace does not, move the investigation to the external rail.
Let arithmetic name the missing leg
“Two-sided” does not mean “two rows.” Suppose a captured sale is 10,000 minor units, a fee is 300, and the payable amount is 9,700. The invariant is not that every row has an equal opposite row in the same report. It is:
capture = payable + fee
Then the payout invariant may be:
payout = Σ payable items + Σ adjustments
Stripe's public payout reconciliation schema exposes gross, fee and net amounts, associates balance transactions with an automatic payout ID, and keeps an ending-balance section for transactions not yet settled at the report cut-off.8 Those fields illustrate why the reconciliation grain must be explicit. Transaction-level conservation and batch-level conservation are different controls.
Run the equations per currency. ISO 4217 standardizes currency codes and records the relationship between currencies and their minor units.9 Do not silently net USD against EUR, and do not let binary floating-point rounding decide whether money exists. Represent amounts in the currency's defined unit and make conversion a separate, fully identified economic event with its own legs.
At this point, the exception queue can be narrow and descriptive: missing counterpart, duplicate counterpart, amount mismatch, currency mismatch, illegal transition, or stale pending state. Idempotency reduces one class of duplicates by allowing a failed API request to be retried under the same key without performing the operation twice.10 It does not replace reconciliation. A request can be unique while a downstream report is missing, late or mapped to the wrong parent.
Preserve the evidence, then correct forward
Do not repair an investigation by editing the incriminating row. Immutable history lets an operator reconstruct what the system believed at any earlier point. Stripe documents its balance transactions as immutable and represents a refund with a new transaction that negates the original; its engineering Ledger is likewise described as an immutable event log from which past state can be reconstructed.211 Corrections should therefore be new, linked entries: reversal, repost, remap or explicit adjustment.
Close the case at two boundaries. First, prove that every internal journal transaction balances and every lifecycle state has a valid resolution. Second, reconcile the internal edge to independent external evidence. A payout is not confirmed merely because its internal status says paid; match the batch's components to the amount observed at the bank boundary. Public reconciliation guidance likewise treats a payout as a batch of underlying transactions to be matched with the deposit received in the bank account.8
The final incident record should contain the broken invariant, the earliest missing edge, the amounts and currencies on both sides, the relevant clocks, every reference used, and the compensating entry if one was required. That packet distinguishes four very different outcomes: money moved but its representation was absent; the representation arrived late; the event was attached to the wrong parent; or the movement itself never completed.
That is how to find the money that lost its other half. Not by staring harder at a total, but by making every movement state whom it came from, where it must go, how much must survive the trip, and when its counterpart becomes overdue.
Sources
“Bookkeeping and accounting.” Stripe Atlas. https://stripe.com/guides/atlas/bookkeeping-and-accounting (opens in a new tab)
“Ledger: Stripe’s system for tracking and validating money movement.” Stripe Developer Blog. https://stripe.dev/blog/ledger-stripe-system-for-tracking-and-validating-money-movement (opens in a new tab)
“Payments lifecycle.” Adyen Docs. https://docs.adyen.com/account/payments-lifecycle/ (opens in a new tab)
“Two-Phase Transfers.” TigerBeetle Documentation. https://docs.tigerbeetle.com/coding/two-phase-transfers/ (opens in a new tab)
“Reporting.” Adyen Docs. https://docs.adyen.com/platforms/quickstart-guide/reporting (opens in a new tab)
“Change or Verify Account Identification — Message Definition Report, Maintenance 2020–2021.” ISO 20022. https://www.iso20022.org/sites/default/files/2020-12/ISO20022_MDRPart2_ChangeOrVerifyAccountIdentification_2020_2021_v1_ForSEGReview.pdf (opens in a new tab)
“What is a Unique End-to-end Transaction Reference (UETR)?” Swift. https://www.swift.com/es/node/310011 (opens in a new tab)
“Payout reconciliation report.” Stripe Documentation. https://docs.stripe.com/reports/payout-reconciliation (opens in a new tab)
“ISO 4217:2015 — Codes for the representation of currencies.” International Organization for Standardization. https://www.iso.org/standard/64758.html (opens in a new tab)
“Idempotent requests.” Stripe API Reference. https://docs.stripe.com/api/idempotent_requests (opens in a new tab)
“Reporting and reconciliation.” Stripe Documentation. https://docs.stripe.com/plan-integration/get-started/reporting-reconciliation (opens in a new tab)