Every canonical field below has a corresponding entry in the
Canonical Field Registry, which is also the contract import
profiles use to map external files into these models.
1. PaymentIntent — what should happen
APaymentIntent is the expectation: a client telling ReconLayer “this payment should occur,
here’s what it should look like.” It is created via
POST /v1/payment-intents, or
indirectly from a client_internal_ledger file import.
Key fields (from PaymentIntentCreateRequest / PaymentIntentSnapshot):
Creating a
PaymentIntent always creates exactly one ReconciliationCase in the same
transaction (status: 'open', expectedAmount copied from sourceAmount). The
create response returns both
paymentIntentId and caseId, with status: 'reconciling' and verdict: null — the case
hasn’t been evaluated yet because no evidence has arrived.Replay and conflict behavior
POSTing the sameexternalReference again is idempotent at the business level:
- If the canonical fields (
sourceAmount,sourceCurrency,destinationAmount,destinationCurrency,beneficiaryAccount, and — if previously set —stablecoin/chain) match the stored intent, the request is treated as a replay: the existing intent is updated (status,beneficiaryName,paymentType, etc.), apayment_intent.replayedaudit event is written, and the response hasoutcome: 'reused'. - If those canonical fields don’t match, the service throws an
ExpectationConflictErrordescribing the field-by-fieldmismatchesbetween the existing and incoming request — the client is asked to resolve the discrepancy rather than silently overwriting it.
2. RawRecord — what arrived, untouched
ARawRecord is the immutable evidence layer. Every inbound payload — a provider webhook, an
on-chain indexer event, a row from an uploaded file, or a manual entry — is stored as one
RawRecord before any normalization happens.
RawRecord.payload is the audit anchor: when something looks wrong in a FlowLeg, you can always
trace it back to the exact bytes that were ingested.
3. FlowLeg — where value actually moved
AFlowLeg is one normalized movement derived from a RawRecord. Complex cross-border
payments produce multiple legs for a single PaymentIntent.
reconciliationScope and shouldMatchLeg
The reconciliation engine’s shouldMatchLeg() helper treats any leg with
reconciliationScope: 'ignored' as invisible to matching — it is never selected as a match
candidate and never blocks case completeness. required and optional legs are both eligible for
matching; the distinction matters for requiredLegsTotal / requiredLegsPresent reporting on the
case summary (see below).
4. FlowLegReference — typed long-tail identifiers
FlowLegReference rows attach typed identifiers to a FlowLeg without adding nullable columns to
the leg table itself. Each row is { type, value }, e.g. { type: "bank_reference", value: "BANK-OUT-789" } or { type: "external_reference", value: "ACME-2026-0001" }.
The matcher specifically looks for a leg reference with type: 'external_reference' as one of its
candidate identifiers (see Reconciliation Rules).
5. ReconciliationCase — the truth row
Exactly oneReconciliationCase exists per PaymentIntent (1:1, enforced by a unique constraint
on paymentIntentId). It is the record operators review, resolve, or archive.
The verdict field
verdict is not stored — it’s derived at read time from reconciliationStatus and
exceptionType:
sla_risk and delayed as verdict values for
filtering (ReconciliationCaseExportVerdict enum: matched, matched_with_exception,
needs_review, unreconciled, sla_risk, delayed). slaRisk itself is a separate boolean on
the case summary: true when the case is open and has been open for more than 240 minutes
(SLA_RISK_THRESHOLD_MINUTES).
ExceptionType enum
Evidence coverage and flow legs on the case summary
GET /v1/reconciliation-cases and
GET /v1/reconciliation-cases/{caseId}
both return a computed evidenceCoverage object and a flowLegs array summarizing the intent’s
legs:
evidenceCoverage values are present | missing | warning | failed | not_provided, derived from
leg status: confirmed → present, pending → warning, failed/reversed → failed,
missing → missing, and not_provided when no leg of that type exists at all (file coverage
is always not_provided today — no leg type maps to it yet).
MatchLink — why evidence was linked
MatchLink is the explainable join between a ReconciliationCase, a FlowLeg, and a
RawRecord. Each row records:
matchType:provider_id,tx_hash,reference_exact,amount_and_time_window, ormanual_override.matchReason: a human-readable sentence, e.g. “External reference ACME-2026-0001 matched the expected payment.”confidence:deterministic,heuristic, ormanual.
(caseId, legId, rawRecordId) triple is unique — re-processing the same evidence against the
same case is idempotent (upsert).
AuditEvent — the append-only narrative
Every meaningful state change writes anAuditEvent: payment_intent.created,
payment_intent.replayed, match.created, match.none, case.evaluated, and more. Each event
carries eventType, actor (e.g. system:matcher, user:<clerk-id>), an optional message, and
a payload JSON blob with event-specific context. Audit events are surfaced on both
PaymentIntentDetail.auditEvents and ReconciliationCaseDetail.auditEvents.
Putting it together
How ReconLayer Works
Conceptual walkthrough of the expectation → evidence → case lifecycle.
Reconciliation Rules
How tolerances, time windows, SLAs, and match strategies are configured.
Database Architecture
The Prisma schema behind these objects.
Canonical Field Registry
Every mappable field, by model and source type.
