Lending & Credit Lines
Build revolving facilities with atomic draw reservations, receivables, repayments, reversals, and write-offs on Parix.
Overview
This guide implements an operational ledger for a revolving credit line. It keeps two different meanings separate:
- Facility capacity records the approved commitment, capacity available to draw, posted principal exposure, over-limit exposure, retired capacity, and charged-off exposure.
- USD money records performing and charged-off principal, interest, and fee receivables together with disbursement, collection, and unapplied-payment positions.
The examples approve a $5,000.00 facility, reserve a maximum $2,500.00 draw, post an actual $2,400.00 disbursement, accrue $48.00 of interest and $12.00 of fees, and allocate a $500.00 repayment. All amounts are positive integer cents. Replace every ledger, code, timeout, and ID with values from your governed lending program, and never reuse the example IDs in more than one database.
Parix and TigerBeetle provide durable balances, account constraints, linked atomic batches, pending-transfer lifecycles, and immutable history. They do not underwrite a borrower, calculate APR, determine a legally valid allocation order, execute a bank payment, service a loan, produce disclosures, or replace a general ledger.
This is a how-to manual for backend, ledger, lending-platform, finance, risk, and operations engineers. It focuses on revolving drawdowns, accrual positions, repayment allocation, returns, delinquency controls, write-offs, and recovery. Installment schedules, collateral, securitization, regulatory capital, tax, and jurisdiction-specific lending rules remain outside its scope.
Architecture and ownership
Keep regulated and distributed-system boundaries explicit:
| Component | Owns |
|---|---|
| Lending application | Borrower and facility state, underwriting, approved limits, draw eligibility, stable business IDs, repayment allocation, schedules, delinquency, statements, notices, and customer-facing balances |
| Pricing and servicing policy | APR, day-count convention, compounding, grace periods, caps, fee eligibility, allocation order, rounding, maturity, forbearance, and policy versions |
| Identity, risk, and compliance | KYC/KYB, sanctions, fraud, affordability, adverse-action workflows, consent, disclosures, collections rules, and evidence retention |
| Payout and collection providers | External disbursement, repayment execution, reversals, finality, settlement files, and provider event IDs |
| Parix | Authenticated public API routing and the managed TigerBeetle database |
| TigerBeetle | Account constraints, pending and posted balances, linked same-request atomicity, immutable transfers, and ordered history |
| Finance and lending operations | Cash and receivable reconciliation, suspense, charge-off approval, recovery treatment, allowance methodology, general-ledger export, and incident repair |
Store borrower identity, application data, credit decisions, bank details, provider payloads, loan agreements, schedules, disclosures, and case notes outside TigerBeetle. Ledger IDs and user-data fields must contain only opaque correlations.
The server integration path is:
lending service -> Parix HTTPS API -> authenticated private gateway -> managed TigerBeetleA linked chain makes only its TigerBeetle transfers atomic. It cannot include an underwriting database transaction, payout-provider call, collection, notification, or general-ledger posting. Model those boundaries as a durable saga or outbox and reconcile each side by stable identifiers.
Ledger model
Units and ledgers
| Ledger | Unit | Purpose |
|---|---|---|
7501 | Credit-line capacity in USD cents | Approved, available, drawn, over-limit, retired, charged-off, and recovered facility capacity |
840 | Actual USD cents | Performing and charged-off principal, interest, and fees plus clearing and unapplied payments |
The capacity ledger is a commitment measure, not cash. The USD ledger represents monetary positions. A linked chain may contain transfers from both ledgers, but each individual transfer's accounts and ledger must match.
The examples use account flag 8 for history, flag 2 for debits_must_not_exceed_credits, and flag 4 for credits_must_not_exceed_debits. A constrained credit-normal account therefore uses flags 10; a constrained debit-normal account uses flags 12.
Facility-capacity accounts
Create the borrower-specific accounts separately for every facility and currency.
| Role | Example ID | Code | Flags | Balance meaning |
|---|---|---|---|---|
| Program limit source | 97000000000000000001 | 400 | 8 | Governed source for approved commitments |
| Available facility | 97000000000000000002 | 410 | 10 | Capacity that may still be drawn; pending debits reserve it |
| Drawn exposure | 97000000000000000003 | 420 | 10 | Posted principal backed by the approved facility |
| Over-limit exposure | 97000000000000000004 | 425 | 10 | Recreated or adjusted principal outside currently available capacity |
| Revoked or retired capacity | 97000000000000000005 | 430 | 10 | Limit removed or principal retired without reopening the facility |
| Charged-off exposure | 97000000000000000006 | 440 | 10 | Principal exposure written out of performing balances but not yet recovered |
| Reviewed adjustment source | 97000000000000000007 | 450 | 8 | Controlled source for unavoidable over-limit reinstatements and reviewed repair |
| Recovered charged-off exposure | 97000000000000000008 | 445 | 10 | Net charged-off principal recovered without reopening drawable capacity |
The available-facility constraint is the concurrent credit-limit control. An application balance read can improve an error message, but it cannot replace the constraint.
USD accounts
| Role | Example ID | Code | Flags | Balance meaning |
|---|---|---|---|---|
| Disbursement clearing | 97000000000000000011 | 500 | 8 | Reconciled to payout-provider events and settlement files |
| Collection clearing | 97000000000000000012 | 501 | 8 | Reconciled to repayment-provider events and settlement files |
| Principal receivable | 97000000000000000013 | 510 | 12 | Debit-normal posted principal owed by the borrower |
| Interest receivable | 97000000000000000014 | 511 | 12 | Debit-normal accrued and unpaid interest |
| Fee receivable | 97000000000000000015 | 512 | 12 | Debit-normal assessed and unpaid fees |
| Unapplied repayment | 97000000000000000016 | 520 | 10 | Confirmed collections that remain available for allocation |
| Interest accrual control | 97000000000000000017 | 530 | 10 | Constrained source for approved interest waivers and refunds |
| Fee assessment control | 97000000000000000018 | 531 | 10 | Constrained source for approved fee waivers and refunds |
| Charged-off principal receivable | 97000000000000000019 | 540 | 12 | Unrecovered principal removed from performing receivables |
| Charged-off interest receivable | 97000000000000000020 | 541 | 12 | Unrecovered interest removed from performing receivables |
| Charged-off fee receivable | 97000000000000000021 | 542 | 12 | Unrecovered fees removed from performing receivables |
| Lending suspense | 97000000000000000022 | 550 | 8 | Reviewed discrepancies with an owner, reason, source evidence, and aging deadline |
The debit-normal constraint on performing receivables prevents aggregate repayment, waiver, or write-off credits from exceeding the amount originated or accrued. The same constraint on charged-off receivables caps aggregate net recoveries and write-off reversals at the facility's current charged-off position. The application must still bind every adjustment to its original charge, receipt, or write-off and enforce its cumulative per-source cap. The unapplied-repayment constraint prevents concurrent allocators or refunds from consuming more confirmed cash than is available.
Clearing and suspense accounts are application-governed reconciliation boundaries whose balances may move on either side as external truth arrives. Accrual and assessment controls are instead credit-normal and constrained, which bounds aggregate waivers and refunds by the corresponding charges already posted. Per-charge uniqueness and cumulative adjustment limits remain application-owned unless the program deliberately creates a separate control account for every charge.
Balance formulas
Use TigerBeetle's separate pending and posted fields instead of collapsing lifecycle states:
available_to_draw =
available.credits_posted
- available.debits_posted
- available.debits_pending
authorized_not_disbursed = principal_receivable.debits_pending
principal_outstanding =
principal_receivable.debits_posted
- principal_receivable.credits_posted
drawn_exposure =
drawn.credits_posted
- drawn.debits_posted
overlimit_exposure =
overlimit.credits_posted
- overlimit.debits_posted
charged_off_principal_outstanding =
charged_off_principal.debits_posted
- charged_off_principal.credits_posted
recovered_principal_exposure =
recovered.credits_posted
- recovered.debits_posted
unapplied_available =
unapplied.credits_posted
- unapplied.debits_posted
- unapplied.debits_pendingFor a normal open revolver without a reconciliation break, posted principal equals drawn exposure plus explicit over-limit exposure. Interest and fees remain separate and never reopen facility capacity unless the legal contract explicitly capitalizes them through a separately modeled event.
Transfer codes
Keep codes immutable even when several events use the same accounts.
| Ledger | Code | Event | Debit → credit |
|---|---|---|---|
7501 | 4000 | Approve or increase limit | Limit source → available |
7501 | 4010 | Draw authorization lifecycle | Available → drawn |
7501 | 4020 | Revolving principal repayment | Drawn → available |
7501 | 4021 | Closed-line principal repayment | Drawn → revoked |
7501 | 4022 | Over-limit principal repayment | Over-limit → adjustment source |
7501 | 4030 | Reduce, freeze, or close unused limit | Available → revoked |
7501 | 4040 | Charge off principal exposure | Drawn or over-limit → charged-off |
7501 | 4041 | Recover charged-off principal | Charged-off → recovered |
7501 | 4050 | Reverse an approved charge-off | Charged-off → drawn or over-limit |
7501 | 4051 | Reverse a principal recovery | Recovered → charged-off |
7501 | 4060 | Reconsume capacity after payment return | Available → drawn |
7501 | 4061 | Record payment-return shortfall | Adjustment source → over-limit |
7501 | 4090 | Reviewed facility adjustment | Approved direction only |
840 | 5010 | Draw principal lifecycle | Principal receivable → disbursement clearing |
840 | 5020 | Repayment received | Collection clearing → unapplied repayment |
840 | 5030 | Allocate fee | Unapplied repayment → fee receivable |
840 | 5031 | Allocate interest | Unapplied repayment → interest receivable |
840 | 5032 | Allocate principal | Unapplied repayment → principal receivable |
840 | 5040 | Accrue interest | Interest receivable → interest accrual control |
840 | 5041 | Assess fee | Fee receivable → fee assessment control |
840 | 5042 | Waive unpaid interest | Interest control → interest receivable |
840 | 5043 | Waive unpaid fee | Fee control → fee receivable |
840 | 5044 | Create paid-charge refund credit | Matching control → unapplied repayment |
840 | 5045 | Pay approved charge refund | Unapplied repayment → collection clearing |
840 | 5050 | Return a posted draw | Disbursement clearing → principal receivable |
840 | 5060 | Write off principal | Charged-off principal → principal receivable |
840 | 5061 | Write off interest | Charged-off interest → interest receivable |
840 | 5062 | Write off fee | Charged-off fee → fee receivable |
840 | 5065 | Reverse an approved write-off | Performing → matching charged-off receivable |
840 | 5070 | Recover charged-off principal | Collection clearing → charged-off principal |
840 | 5071 | Recover charged-off interest | Collection clearing → charged-off interest |
840 | 5072 | Recover charged-off fee | Collection clearing → charged-off fee |
840 | 5075 | Reverse an approved recovery | Charged-off receivable → collection clearing |
840 | 5080 | Reverse principal allocation | Principal receivable → unapplied repayment |
840 | 5081 | Reverse interest allocation | Interest receivable → unapplied repayment |
840 | 5082 | Reverse fee allocation | Fee receivable → unapplied repayment |
840 | 5083 | Reverse repayment receipt | Unapplied repayment → collection clearing |
840 | 5090 | Reviewed monetary adjustment | Approved direction only |
Drawdown lifecycle
Persist the facility decision, draw request, maximum approved amount, exact two-leg reservation payload, provider reference, and every alternative terminal ID before calling Parix.
Reserve both lifecycle positions in one linked pair:
- Pending capacity moves available facility to drawn exposure.
- Pending USD principal moves principal receivable to disbursement clearing.
Only the constrained available-capacity leg admits or rejects the draw against the credit limit. The USD leg records authorized-but-not-disbursed principal and keeps the two internal lifecycle records atomic; it does not reserve bank, provider, or treasury liquidity. Add a separate constrained funding-capacity position only when the program intentionally needs that distinct control.
The application may dispatch the payout provider only after exact lookup proves that both reservations exist. When the provider outcome is final, either partial-post the exact positive amount to both pending transfers or void both original amounts. Every non-final item carries linked; the final item does not.
A partial post is terminal and releases both unused remainders. A void uses the original full amount. If the provider succeeds after the pending pair expires, do not invent a replacement authorization to conceal the lapse. Preserve the provider evidence and route the discrepancy to a reviewed late-disbursement or suspense workflow.
The pending timeout is seconds and represents only an in-flight draw reservation. It is not a payment due date, grace period, maturity, delinquency clock, or facility expiration.
Interest, fees, and repayment allocation
The application calculates finalized interest and fees with integer arithmetic and persists the contract version, rate inputs, day-count convention, accrual period, caps, rounding, and source event. TigerBeetle receives only the resulting positive cents:
- interest debits interest receivable and credits interest accrual control;
- a fee debits fee receivable and credits fee assessment control.
After verified collection finality, post collection clearing to unapplied repayment with one provider-event-derived stable ID. Then persist the exact allocation and submit its nonzero legs as one linked chain. The example allocation order is fee, interest, then principal; your legal contract and jurisdiction may require another order.
Only a principal allocation changes capacity:
- an open revolver moves drawn exposure back to available capacity;
- a closed or non-revolving line moves drawn exposure to revoked capacity;
- an over-limit facility reduces explicit over-limit exposure before reopening any capacity.
Require allocated fee + interest + principal to be no greater than confirmed unapplied cash. Leave a residual unapplied until another approved allocation or refund workflow. Never allocate pending principal or an unconfirmed provider payment.
An unpaid interest or fee waiver debits its constrained control and credits the matching performing receivable, so the facility's aggregate charge budget and current receivable cannot be exceeded. Persist a unique original-charge-to-adjustment mapping and reject cumulative waivers and refunds above that individual charge. A charge already paid has no receivable left to credit. Refund it instead with a new approved linked chain from the matching control to unapplied repayment and then from unapplied repayment to collection clearing; the provider payout remains a separate reconciled saga.
Returns, delinquency, write-off, and recovery
A returned posted draw is a new linked compensation from disbursement clearing to principal receivable and from drawn exposure to available or revoked capacity. It never voids the original posted draw.
A repayment return can recreate debt after the borrower has already redrawn the restored capacity. Under a per-facility durable lock:
- reverse fee, interest, and principal allocations into unapplied repayment;
- reverse the unapplied receipt to collection clearing;
- split returned principal into
backed = min(returned principal, available_to_draw)andoverlimit = returned principal - backed; - move backed capacity from available to drawn and any remainder from adjustment source to explicit over-limit exposure; and
- persist a new allocation-attempt version and submit every nonzero leg as one linked chain.
If a concurrent constraint rejection occurs, none of the chain commits. Reconcile the stable IDs, recompute from current balances under the facility lock, allocate new IDs for a new attempt version, and retain the rejected plan as evidence.
Delinquency, forbearance, and collections states remain application-owned. To make a draw freeze race-safe at the ledger boundary, move currently unused available capacity to revoked capacity with code 4030 after reconciling in-flight draws. Reinstatement requires a new, explicit underwriting decision and limit transfer.
A write-off chain debits each matching charged-off receivable, credits the performing receivable being removed, and moves the same principal from drawn or over-limit exposure to charged-off capacity. A full-close write-off may also retire unused available capacity after a separate approved limit decision; a partial accounting write-off moves only the affected exposure. It does not reopen the line, delete debt history, waive legal rights automatically, or decide general-ledger accounting.
A later recovery debits collection clearing and credits the matching constrained charged-off receivable, which caps aggregate net recovery at the facility's current charged-off position. Persist a unique write-off-to-recovery mapping and reject cumulative recovery above that original write-off. Principal recovery also moves the same amount from charged-off capacity to recovered charged-off exposure in the linked chain. Recovery never credits available capacity.
Reverse a recovery with new opposite-direction money and, for principal, capacity legs; never rewrite the original event. Before writing code 5075, lock the facility, require a unique original-recovery-to-reversal mapping, and reject cumulative reversal above that recovery's posted amount. The principal recovered-capacity constraint provides an additional aggregate backstop; interest and fee reversal caps are application-enforced unless those categories use separate constrained recovery counters.
Invariants
| Invariant | Enforcement |
|---|---|
| Concurrent draws cannot exceed unused capacity | Available facility uses flags 10; reserve before provider dispatch |
| Principal and capacity move together | Reserve, post, void, posted-draw return, principal allocation, and write-off use one correctly terminated linked chain |
| Repayment cannot over-allocate | Unapplied repayment uses flags 10; receivables use flags 12; allocation contains only positive, persisted legs |
| Only principal reopens a revolving line | Fee and interest allocation remain entirely on ledger 840; principal adds the capacity leg |
| Limit decreases never erase debt | Limit changes move only available capacity; they never alter principal, interest, or fee receivables |
| Recovery stays bounded | Application caps each source write-off; flags 12 cap aggregate net recovery; recovered principal is non-drawable |
| Pending timeout has one meaning | It releases only an unfinished draw reservation; the application owns every contractual date |
| Corrections remain immutable | Return, reversal, waiver, write-off, recovery, and adjustment use new stable IDs and named codes |
| External execution is not ledger-atomic | Provider calls and application SQL state use durable saga, outbox, idempotency, and reconciliation |
| Retries preserve exact intent | Persist payload order and all immutable fields; exact lookup precedes replay after conflict or ambiguity |
| Sensitive lending records stay outside the ledger | Use opaque correlations only; never store PII, bank details, decisions, documents, or unrestricted provider data |
Before you begin
- Select an eligible environment. Developer and Dedicated Single Node are non-production plans. Confirm current production topology, quota, backup, pending-timeout, and support requirements in Plans and limits.
- Create a disposable database for this walkthrough, wait for Ready, and record its UUID. API and CLI paths use the UUID rather than the display name.
- Obtain program approval for the capacity and money ledgers, account directions, code registry, receivable and clearing conventions, write-off treatment, and general-ledger export.
- Define underwriting, credit-limit, draw, payout, repayment, delinquency, payment-return, write-off, recovery, and suspense state machines before accepting real funds.
- Define APR, interest, fee, day-count, allocation, rounding, overpayment, and late-event policies. Version the exact inputs used by every monetary event.
- Define provider-finality and reconciliation rules for disbursement and collection. A provider success and a ledger success are separate facts.
- Allocate opaque stable account and transfer IDs before the first write. Persist
(facility, business event, leg kind, attempt version) -> transfer ID + immutable payload. - Generate a database-scoped API key for the server application and keep it only in server-side secret storage. Dashboard and CLI users authenticate separately.
- Confirm that the active plan permits both external ledgers and the largest linked chain. Never split a chain merely to fit a lower event-per-request limit.
- Use decimal strings for wide JSON IDs and amounts. The public schema accepts batches of 1–8,190 items, while Shared and plan limits can be lower.
@parix/tigerbeetle-node is the published Node.js client for TigerBeetle-shaped operations on the Parix HTTPS gateway. It does not open a native TigerBeetle connection. Pin a package version approved for your environment, configure { baseUrl, apiKey, databaseId } with a database-scoped API key, keep the client behind an application boundary, and validate behavior before production traffic.
Dashboard walkthrough
Live-write warning: Query explorer writes persist immediately and cannot be edited or deleted. Confirm the organization, database UUID, and non-production environment before selecting Run.
- Open the intended database and select Query.
- Choose Create accounts, disable random-ID generation, and create the governed account registry. Use ledger
7501for facility positions and840for USD positions. - Use flags
10for credit-normal constrained capacity, unapplied repayment, interest-accrual control, and fee-assessment control accounts. Use flags12for performing and charged-off debit-normal receivables. Use flags8only for the program and adjustment sources, clearing, and suspense accounts in this registry. - Confirm an empty create-result array. For any nonempty result, map its index to the submitted account and then look up every stable ID before deciding whether it is a replay or a rejection.
- Approve the test limit with a posted code-
4000transfer from limit source to available facility. - Create the two draw reservations in one request. The first transfer has flags
3(pending | linked); the final transfer has flags2(pending). Use the same timeout and draw correlation. - After the simulated payout outcome, create either the linked post pair or the linked void pair. Never execute both terminal alternatives.
- Use Lookup transfers to verify all stable IDs and fields. Use Query accounts separately with ledger
7501and ledger840; Shared queries reject a missing ledger.

Create every account with a reviewed stable ID. The screenshot illustrates the operator surface; the IDs and code shown in your browser depend on the selected database and input.

Query capacity and USD independently. On Shared Developer, API and CLI query_* calls require an explicit ledger filter (plan_restricted if omitted). The Dashboard may offer All known ledgers as a fan-out convenience; still prefer an explicit governed ledger when you need a scoped smoke query.
The Dashboard is appropriate for controlled smoke tests and investigation. Production underwriting, servicing, payout, and collection services use scoped server credentials and durable application workflows.
CLI walkthrough
The commands use the latest published @parix/cli syntax. The CLI is an operator and development surface authenticated through browser OAuth; never reuse its local session as a production service credential.
Install, authenticate, and select the disposable database:
npm install -g @parix/cli@latest
parix --version
parix auth login
parix auth status
parix database list --json
export PARIX_DATABASE_ID="db_replace_with_uuid"Create the account registry
Save the following bare array as lending-accounts.json.
[
{ "id": "97000000000000000001", "ledger": 7501, "code": 400, "flags": 8 },
{ "id": "97000000000000000002", "ledger": 7501, "code": 410, "flags": 10 },
{ "id": "97000000000000000003", "ledger": 7501, "code": 420, "flags": 10 },
{ "id": "97000000000000000004", "ledger": 7501, "code": 425, "flags": 10 },
{ "id": "97000000000000000005", "ledger": 7501, "code": 430, "flags": 10 },
{ "id": "97000000000000000006", "ledger": 7501, "code": 440, "flags": 10 },
{ "id": "97000000000000000007", "ledger": 7501, "code": 450, "flags": 8 },
{ "id": "97000000000000000008", "ledger": 7501, "code": 445, "flags": 10 },
{ "id": "97000000000000000011", "ledger": 840, "code": 500, "flags": 8 },
{ "id": "97000000000000000012", "ledger": 840, "code": 501, "flags": 8 },
{ "id": "97000000000000000013", "ledger": 840, "code": 510, "flags": 12 },
{ "id": "97000000000000000014", "ledger": 840, "code": 511, "flags": 12 },
{ "id": "97000000000000000015", "ledger": 840, "code": 512, "flags": 12 },
{ "id": "97000000000000000016", "ledger": 840, "code": 520, "flags": 10 },
{ "id": "97000000000000000017", "ledger": 840, "code": 530, "flags": 10 },
{ "id": "97000000000000000018", "ledger": 840, "code": 531, "flags": 10 },
{ "id": "97000000000000000019", "ledger": 840, "code": 540, "flags": 12 },
{ "id": "97000000000000000020", "ledger": 840, "code": 541, "flags": 12 },
{ "id": "97000000000000000021", "ledger": 840, "code": 542, "flags": 12 },
{ "id": "97000000000000000022", "ledger": 840, "code": 550, "flags": 8 }
]parix tb create-accounts "$PARIX_DATABASE_ID" --file ./lending-accounts.json --jsonAn empty result array means the create request reported no item errors. Look up and compare the intended account fields before treating a replay or ambiguous response as complete.
Approve the facility
Save facility-approval.json. This grants 500,000 capacity cents without creating cash or debt.
[
{
"id": "97100000000000000001",
"debit_account_id": "97000000000000000001",
"credit_account_id": "97000000000000000002",
"amount": "500000",
"pending_id": "0",
"user_data_128": "97200000000000000001",
"user_data_64": "973000000000000001",
"user_data_32": 1,
"timeout": 0,
"ledger": 7501,
"code": 4000,
"flags": 0,
"timestamp": "0"
}
]Single-leg facility grants can use flag-driven CLI fields. The file form below is equivalent for reviewed automation; do not submit both for the same stable ID. Optional user_data_* in the JSON is intentional only where this manual later queries or groups by facility request and policy version:
user_data_128— facility or draw request identity (“what”)user_data_64— policy or contract version pointeruser_data_32— small leg/role enum within a multi-leg chain when useful
Leave them zero when lookup-by-transfer-id is enough. See Optional user_data fields.
# Flag-driven form (user_data omitted = zero):
parix tb create-transfers "$PARIX_DATABASE_ID" \
--id 97100000000000000001 \
--from 97000000000000000001 \
--to 97000000000000000002 \
--amount 500000 \
--ledger 7501 \
--code 4000 \
--json
# Or: parix tb create-transfers "$PARIX_DATABASE_ID" --file ./facility-approval.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" --id 97100000000000000001 --jsonReserve and post a draw
Save draw-reserve.json. Timeout 600 means ten minutes and applies only to provider finalization.
[
{
"id": "97100000000000000010",
"debit_account_id": "97000000000000000002",
"credit_account_id": "97000000000000000003",
"amount": "250000",
"pending_id": "0",
"user_data_128": "97200000000000000010",
"user_data_64": "973000000000000002",
"user_data_32": 1,
"timeout": 600,
"ledger": 7501,
"code": 4010,
"flags": 3,
"timestamp": "0"
},
{
"id": "97100000000000000011",
"debit_account_id": "97000000000000000013",
"credit_account_id": "97000000000000000011",
"amount": "250000",
"pending_id": "0",
"user_data_128": "97200000000000000010",
"user_data_64": "973000000000000002",
"user_data_32": 2,
"timeout": 600,
"ledger": 840,
"code": 5010,
"flags": 2,
"timestamp": "0"
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./draw-reserve.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 97100000000000000010 \
--id 97100000000000000011 \
--jsonDispatch the payout only after both reservations exact-match. Assume the provider confirms 240,000 cents. Save draw-post.json; the partial posts release the unused 10,000 from each reservation.
[
{
"id": "97100000000000000012",
"debit_account_id": "97000000000000000002",
"credit_account_id": "97000000000000000003",
"amount": "240000",
"pending_id": "97100000000000000010",
"user_data_128": "97200000000000000010",
"user_data_64": "973000000000000002",
"user_data_32": 1,
"timeout": 0,
"ledger": 7501,
"code": 4010,
"flags": 5,
"timestamp": "0"
},
{
"id": "97100000000000000013",
"debit_account_id": "97000000000000000013",
"credit_account_id": "97000000000000000011",
"amount": "240000",
"pending_id": "97100000000000000011",
"user_data_128": "97200000000000000010",
"user_data_64": "973000000000000002",
"user_data_32": 2,
"timeout": 0,
"ledger": 840,
"code": 5010,
"flags": 4,
"timestamp": "0"
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./draw-post.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 97100000000000000012 \
--id 97100000000000000013 \
--jsonIf the provider definitively fails instead, create this alternative linked void pair with preallocated IDs. Do not submit it after a post has been attempted or accepted.
[
{
"id": "97100000000000000014",
"debit_account_id": "97000000000000000002",
"credit_account_id": "97000000000000000003",
"amount": "250000",
"pending_id": "97100000000000000010",
"user_data_128": "97200000000000000010",
"user_data_64": "973000000000000002",
"user_data_32": 1,
"timeout": 0,
"ledger": 7501,
"code": 4010,
"flags": 9,
"timestamp": "0"
},
{
"id": "97100000000000000015",
"debit_account_id": "97000000000000000013",
"credit_account_id": "97000000000000000011",
"amount": "250000",
"pending_id": "97100000000000000011",
"user_data_128": "97200000000000000010",
"user_data_64": "973000000000000002",
"user_data_32": 2,
"timeout": 0,
"ledger": 840,
"code": 5010,
"flags": 8,
"timestamp": "0"
}
]Accrue and allocate a repayment
After the posted draw, save accrual-and-receipt.json. The interest, fee, and provider-confirmed receipt are independent events; the plain array is not atomic. Reconcile all three stable IDs by lookup.
[
{
"id": "97100000000000000020",
"debit_account_id": "97000000000000000014",
"credit_account_id": "97000000000000000017",
"amount": "4800",
"pending_id": "0",
"user_data_128": "97200000000000000020",
"user_data_64": "973000000000000003",
"user_data_32": 1,
"timeout": 0,
"ledger": 840,
"code": 5040,
"flags": 0,
"timestamp": "0"
},
{
"id": "97100000000000000021",
"debit_account_id": "97000000000000000015",
"credit_account_id": "97000000000000000018",
"amount": "1200",
"pending_id": "0",
"user_data_128": "97200000000000000021",
"user_data_64": "973000000000000004",
"user_data_32": 1,
"timeout": 0,
"ledger": 840,
"code": 5041,
"flags": 0,
"timestamp": "0"
},
{
"id": "97100000000000000022",
"debit_account_id": "97000000000000000012",
"credit_account_id": "97000000000000000016",
"amount": "50000",
"pending_id": "0",
"user_data_128": "97200000000000000022",
"user_data_64": "0",
"user_data_32": 1,
"timeout": 0,
"ledger": 840,
"code": 5020,
"flags": 0,
"timestamp": "0"
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./accrual-and-receipt.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 97100000000000000020 \
--id 97100000000000000021 \
--id 97100000000000000022 \
--jsonThe example policy allocates 1,200 fee cents, 4,800 interest cents, and 44,000 principal cents. Save the complete chain as repayment-allocation.json.
[
{
"id": "97100000000000000030",
"debit_account_id": "97000000000000000016",
"credit_account_id": "97000000000000000015",
"amount": "1200",
"pending_id": "0",
"user_data_128": "97200000000000000030",
"user_data_64": "973000000000000005",
"user_data_32": 1,
"timeout": 0,
"ledger": 840,
"code": 5030,
"flags": 1,
"timestamp": "0"
},
{
"id": "97100000000000000031",
"debit_account_id": "97000000000000000016",
"credit_account_id": "97000000000000000014",
"amount": "4800",
"pending_id": "0",
"user_data_128": "97200000000000000030",
"user_data_64": "973000000000000005",
"user_data_32": 2,
"timeout": 0,
"ledger": 840,
"code": 5031,
"flags": 1,
"timestamp": "0"
},
{
"id": "97100000000000000032",
"debit_account_id": "97000000000000000016",
"credit_account_id": "97000000000000000013",
"amount": "44000",
"pending_id": "0",
"user_data_128": "97200000000000000030",
"user_data_64": "973000000000000005",
"user_data_32": 3,
"timeout": 0,
"ledger": 840,
"code": 5032,
"flags": 1,
"timestamp": "0"
},
{
"id": "97100000000000000033",
"debit_account_id": "97000000000000000003",
"credit_account_id": "97000000000000000002",
"amount": "44000",
"pending_id": "0",
"user_data_128": "97200000000000000030",
"user_data_64": "973000000000000005",
"user_data_32": 4,
"timeout": 0,
"ledger": 7501,
"code": 4020,
"flags": 0,
"timestamp": "0"
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./repayment-allocation.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 97100000000000000030 \
--id 97100000000000000031 \
--id 97100000000000000032 \
--id 97100000000000000033 \
--json
parix tb query-accounts "$PARIX_DATABASE_ID" --ledger 7501 --limit 50 --json
parix tb query-accounts "$PARIX_DATABASE_ID" --ledger 840 --limit 50 --jsonExpected posted positions after the successful path are:
| Position | Expected cents |
|---|---|
| Available facility | 304,000 |
| Drawn exposure | 196,000 |
| Principal receivable | 196,000 |
| Interest receivable | 0 |
| Fee receivable | 0 |
| Unapplied repayment | 0 |
| Disbursement clearing credit | 240,000 |
| Collection clearing debit | 50,000 |
These values prove the tutorial arithmetic only. Reconcile clearing balances to the provider and receivable/control balances to the lending application and finance systems.
Charge-off principal (secondary path)
After policy approval to charge off 10,000 cents of performing principal, submit one linked chain that moves money from principal receivable to charged-off principal and capacity from drawn exposure to charged-off capacity. Persist unique write-off IDs before the first write.
[
{
"id": "97100000000000000040",
"debit_account_id": "97000000000000000019",
"credit_account_id": "97000000000000000013",
"amount": "10000",
"ledger": 840,
"code": 5060,
"flags": 1
},
{
"id": "97100000000000000041",
"debit_account_id": "97000000000000000003",
"credit_account_id": "97000000000000000006",
"amount": "10000",
"ledger": 7501,
"code": 4040,
"flags": 0
}
]parix tb create-transfers "$PARIX_DATABASE_ID" --file ./principal-write-off.json --json
parix tb lookup-transfers "$PARIX_DATABASE_ID" \
--id 97100000000000000040 \
--id 97100000000000000041 \
--jsonRecovery later debits collection clearing into the charged-off receivable and moves principal capacity from charged-off to recovered (never back to available). Use new stable IDs, unique write-off-to-recovery mapping, and the facility lock described in Returns, delinquency, write-off, and recovery.
Node.js implementation
The adapter returns TigerBeetle-shaped records over the Parix HTTPS gateway. It returns indexed create-conflict arrays (HTTP 409 + tbResults is unwrapped into those arrays) and does not expose the raw gateway response's persisted field, so this example requires exact lookup before and after every create. Full void of an unposted draw may use the original reserved amount or amount_max for remaining capacity.
import type { CreateTransferResult, Transfer } from '@parix/tigerbeetle-node';
import { CreateTransferError, TransferFlags, amount_max, createClient } from '@parix/tigerbeetle-node';
const client = createClient({
baseUrl: process.env.PARIX_BASE_URL ?? 'https://parix.io',
apiKey: process.env.PARIX_API_KEY!,
databaseId: process.env.PARIX_DATABASE_ID!,
});
const accounts = {
available: 97000000000000000002n,
drawn: 97000000000000000003n,
overlimit: 97000000000000000004n,
adjustmentSource: 97000000000000000007n,
disbursement: 97000000000000000011n,
collection: 97000000000000000012n,
principal: 97000000000000000013n,
interest: 97000000000000000014n,
fee: 97000000000000000015n,
unapplied: 97000000000000000016n,
} as const;
function linkBatch(batch: readonly Transfer[]): Transfer[] {
if (batch.length === 0) throw new Error('linked batch must not be empty');
return batch.map((transfer, index) => ({
...transfer,
flags: (transfer.flags & ~TransferFlags.linked) | (index < batch.length - 1 ? TransferFlags.linked : 0),
}));
}
function sameTransfer(actual: Transfer, intended: Transfer): boolean {
return (
actual.id === intended.id &&
actual.debit_account_id === intended.debit_account_id &&
actual.credit_account_id === intended.credit_account_id &&
actual.amount === intended.amount &&
actual.pending_id === intended.pending_id &&
actual.user_data_128 === intended.user_data_128 &&
actual.user_data_64 === intended.user_data_64 &&
actual.user_data_32 === intended.user_data_32 &&
actual.timeout === intended.timeout &&
actual.ledger === intended.ledger &&
actual.code === intended.code &&
actual.flags === intended.flags
);
}
async function lookupExactBatch(batch: readonly Transfer[]): Promise<'none' | 'exact'> {
const found = await client.lookupTransfers(batch.map(({ id }) => id));
if (found.length === 0) return 'none';
if (found.length !== batch.length) throw new Error('partial ledger visibility: reconcile manually');
const byId = new Map(found.map((transfer) => [transfer.id, transfer]));
if (
!batch.every((intended) => {
const actual = byId.get(intended.id);
return actual !== undefined && sameTransfer(actual, intended);
})
) {
throw new Error('stable ID exists with different immutable fields');
}
return 'exact';
}
class TransferBatchRejected extends Error {
constructor(readonly results: readonly CreateTransferResult[]) {
const details = results
.map(({ index, result }) => {
const name =
result === CreateTransferError.exceeds_credits
? 'exceeds_credits'
: result === CreateTransferError.exceeds_debits
? 'exceeds_debits'
: result === CreateTransferError.linked_event_failed
? 'linked_event_failed'
: String(result);
return `${index}:${name}`;
})
.join(', ');
super(`TigerBeetle rejected transfer indexes ${details}`);
}
}
async function createOrReconcile(batch: readonly Transfer[]): Promise<void> {
if ((await lookupExactBatch(batch)) === 'exact') return;
let results: CreateTransferResult[];
try {
results = await client.createTransfers([...batch]);
} catch (error) {
if ((await lookupExactBatch(batch)) === 'exact') return;
throw error;
}
if ((await lookupExactBatch(batch)) === 'exact') return;
if (results.length > 0) throw new TransferBatchRejected(results);
throw new Error('create returned without an exact durable batch');
}Reserve a draw before dispatching the provider:
const drawRequestId = 97200000000000000010n;
const drawPolicyVersion = 973000000000000002n;
const maximumDraw = 250_000n;
const capacityReserve: Transfer = {
id: 97100000000000000010n,
debit_account_id: accounts.available,
credit_account_id: accounts.drawn,
amount: maximumDraw,
pending_id: 0n,
user_data_128: drawRequestId,
user_data_64: drawPolicyVersion,
user_data_32: 1,
timeout: 600,
ledger: 7501,
code: 4010,
flags: TransferFlags.pending,
timestamp: 0n,
};
const principalReserve: Transfer = {
...capacityReserve,
id: 97100000000000000011n,
debit_account_id: accounts.principal,
credit_account_id: accounts.disbursement,
user_data_32: 2,
ledger: 840,
code: 5010,
};
const drawReserveBatch = linkBatch([capacityReserve, principalReserve]);
await createOrReconcile(drawReserveBatch);
// Dispatch only after the exact lookup above confirms both reservations.Post the exact provider-confirmed draw or explicitly void both reservations:
const actualDraw = 240_000n;
if (actualDraw < 0n || actualDraw > maximumDraw) {
throw new Error('actual draw is outside the reserved amount');
}
const drawTerminalBatch =
actualDraw === 0n
? linkBatch([
{
...capacityReserve,
id: 97100000000000000014n,
amount: amount_max, // full remaining; maximumDraw also valid for unposted full void
pending_id: capacityReserve.id,
timeout: 0,
flags: TransferFlags.void_pending_transfer,
},
{
...principalReserve,
id: 97100000000000000015n,
amount: amount_max,
pending_id: principalReserve.id,
timeout: 0,
flags: TransferFlags.void_pending_transfer,
},
])
: linkBatch([
{
...capacityReserve,
id: 97100000000000000012n,
amount: actualDraw,
pending_id: capacityReserve.id,
timeout: 0,
flags: TransferFlags.post_pending_transfer,
},
{
...principalReserve,
id: 97100000000000000013n,
amount: actualDraw,
pending_id: principalReserve.id,
timeout: 0,
flags: TransferFlags.post_pending_transfer,
},
]);
await createOrReconcile(drawTerminalBatch);Record a confirmed repayment first, then allocate only positive components:
const repaymentEventId = 97200000000000000022n;
const repaymentReceipt: Transfer = {
id: 97100000000000000022n,
debit_account_id: accounts.collection,
credit_account_id: accounts.unapplied,
amount: 50_000n,
pending_id: 0n,
user_data_128: repaymentEventId,
user_data_64: 0n,
user_data_32: 1,
timeout: 0,
ledger: 840,
code: 5020,
flags: 0,
timestamp: 0n,
};
await createOrReconcile([repaymentReceipt]);
// Hold the per-facility allocation lock while reading and submitting this plan.
const [unappliedAccount] = await client.lookupAccounts([accounts.unapplied]);
if (!unappliedAccount) throw new Error('unapplied account is missing');
const unappliedAvailableSnapshot =
unappliedAccount.credits_posted - unappliedAccount.debits_posted - unappliedAccount.debits_pending;
const allocationId = 97200000000000000030n;
const allocationPolicyVersion = 973000000000000005n;
const fee = 1_200n;
const interest = 4_800n;
const principal = 44_000n;
const allocated = fee + interest + principal;
if (allocated <= 0n || allocated > unappliedAvailableSnapshot) {
throw new Error('allocation is outside confirmed unapplied cash');
}
const allocationCommon = {
pending_id: 0n,
user_data_128: allocationId,
user_data_64: allocationPolicyVersion,
timeout: 0,
flags: 0,
timestamp: 0n,
} as const;
const allocationLegs: Transfer[] = [];
if (fee > 0n) {
allocationLegs.push({
...allocationCommon,
id: 97100000000000000030n,
debit_account_id: accounts.unapplied,
credit_account_id: accounts.fee,
amount: fee,
user_data_32: 1,
ledger: 840,
code: 5030,
});
}
if (interest > 0n) {
allocationLegs.push({
...allocationCommon,
id: 97100000000000000031n,
debit_account_id: accounts.unapplied,
credit_account_id: accounts.interest,
amount: interest,
user_data_32: 2,
ledger: 840,
code: 5031,
});
}
if (principal > 0n) {
allocationLegs.push(
{
...allocationCommon,
id: 97100000000000000032n,
debit_account_id: accounts.unapplied,
credit_account_id: accounts.principal,
amount: principal,
user_data_32: 3,
ledger: 840,
code: 5032,
},
{
...allocationCommon,
id: 97100000000000000033n,
debit_account_id: accounts.drawn,
credit_account_id: accounts.available,
amount: principal,
user_data_32: 4,
ledger: 7501,
code: 4020,
},
);
}
await createOrReconcile(linkBatch(allocationLegs));The snapshot is policy validation under the per-facility allocation lock and intentionally includes any earlier unapplied residual. The account constraint remains the concurrency backstop if another writer bypasses that lock. Persist every included leg and stable ID before the first allocation attempt. Always call client.destroy() in a finally block when the worker lifecycle ends.
Failure and retry handling
| Signal or state | Meaning | Required action |
|---|---|---|
Empty create result [] | The adapter reported no indexed item errors | Still require exact lookup before advancing durable state. The adapter does not expose gateway persisted. |
| Nonempty create result | One or more indexed TigerBeetle conflicts | Map every returned index and classify each result (for example CreateTransferError.exceeds_credits). HTTP 409 + tbResults is unwrapped into this array. Look up every submitted stable ID; classify capacity/receivable constraints separately from defects |
Available-facility exceeds_credits | Draw or limit reduction exceeds unused capacity | Decline or serialize the business operation; never dispatch the payout or retry as infrastructure work |
Receivable exceeds_debits | Allocation, reversal, or write-off exceeds the posted receivable | Stop and reconcile policy inputs, prior allocations, and event ordering |
Charged-off receivable exceeds_debits | Recovery or write-off reversal exceeds the remaining charged-off position | Reject the complete chain; reconcile the original write-off, prior recoveries, reversals, and provider event |
Unapplied exceeds_credits | Allocators attempted to consume more than confirmed collections | Reject the complete allocation chain and serialize/recompute under the facility lock |
Timeout, disconnect, reset, 500, or 503 | The outcome may be absent or committed | Exact lookup every ID first; retry only a fully absent batch with identical order and fields |
HTTP 400 | Strict payload, field, path, or batch validation failed | Fix the producer; do not retry unchanged |
HTTP 401 or 403 | Credential, scope, database boundary, ledger, timeout, or plan policy rejected the call | Correct access or policy; never bypass it with operator credentials |
HTTP 402 | Billing state blocks the operation | Restore eligibility, preserve the intent, and look up before re-driving |
HTTP 429 | Transient admission pressure or durable plan quota | Distinguish the reason; use bounded jitter only for transient pressure and never hot-loop a hard quota |
| Partial lookup or immutable-field mismatch | The stable-ID contract or assumed atomic batch is broken | Quarantine the facility event and reconcile manually; do not generate replacement IDs automatically |
| Draw post or void reports expired/already terminal | Provider and pending lifecycle disagree | Preserve provider truth and ledger history; route to late-disbursement or suspense review |
| Provider payout succeeds, ledger outcome is unknown | Cross-system saga is incomplete | Do not call the provider again; reconcile the original pair and then follow the approved exception workflow |
| Collection succeeds, allocation fails | Cash remains confirmed but unapplied | Keep the receipt immutable and retry a newly persisted allocation plan only after resolving deterministic conflicts |
| Repayment return cannot fully reconsume capacity | Borrower redrew capacity restored by the original principal payment | Split the new plan into backed and explicit over-limit exposure; never suppress recreated debt |
| Write-off chain fails | Receivable, capacity, or close inputs disagree | Commit nothing, reconcile each source balance, and re-approve a new complete write-off plan |
| Natural pending expiry | TigerBeetle released an unfinished reservation | Reconcile provider state and close application work explicitly; do not use expiry as a normal servicing scheduler |
Stable TigerBeetle IDs are the idempotency boundary for operation payloads. Persist exact payloads and their order before first submission. Compare IDs, accounts, amount, pending ID, user data, timeout, ledger, code, and flags on every reconciliation lookup.
The gateway can return a bounded conflict list. An omitted item is not proof of success; resolve every submitted ID. A normal unlinked batch may partially succeed, while a correctly terminated linked chain succeeds or fails as one chain.
Test scenarios
Run these scenarios with synthetic borrowers in an isolated non-production database. Explicitly post or void pending test draws instead of relying on natural expiry.
| Scenario | Expected result |
|---|---|
| Account bootstrap and replay | All 20 accounts exact-match; duplicate provisioning creates no second account |
| Limit approval and increase | Available capacity rises by the exact approved amount once |
| Limit reduction | Only unused capacity moves to revoked; principal and receivables remain unchanged |
| Concurrent draws at the boundary | At most the affordable linked reservation pairs commit |
| Draw second-leg failure | Neither pending capacity nor pending principal exists |
| Draw full post | Drawn exposure and principal receivable rise by the same amount |
| Draw partial post | Exact actual amount posts to both ledgers and both unused remainders release |
| Draw explicit void | Both reservations close atomically with no posted principal |
| Post and void race | One terminal pair wins; the conflicting alternative enters reconciliation |
| Pending expiry | Both pending balances release, but provider and application state remain explicitly unresolved |
| Interest accrual | Receivable and accrual control rise once under the persisted rate and rounding version |
| Fee assessment and unpaid waiver | Fee posts once; waiver cannot exceed either the assessed control or unpaid receivable |
| Paid fee refund | A linked control-to-unapplied-to-clearing chain creates the refund without a negative receivable |
| Repayment receipt replay | One collection-to-unapplied transfer exists for the provider event |
| Allocation order | Fee, interest, and principal match the persisted legal policy snapshot |
| Excess repayment | Residual remains unapplied; no receivable becomes negative |
| Concurrent allocation | Unapplied and receivable constraints reject over-allocation atomically |
| Revolving principal payment | Only principal moves drawn exposure back to available |
| Closed-line principal payment | Principal moves drawn exposure to revoked capacity instead of reopening the line |
| Over-limit principal payment | Explicit over-limit exposure reduces before capacity can reopen |
| Posted draw return | New compensating legs reduce principal and drawn exposure without deleting the original |
| Repayment return before redraw | Receivables and drawn exposure are reinstated in one new linked chain |
| Repayment return after redraw | Available is reconsumed only as far as possible; the remainder becomes explicit over-limit exposure |
| Delinquency freeze | Reconciled unused capacity moves to revoked and stale callers cannot reserve it |
| Principal, interest, and fee write-off | Exact performing receivables move to charged-off positions; principal capacity follows atomically |
| Write-off replay | One immutable charge-off chain exists |
| Recovery | Recovery consumes the matching charged-off receivable; principal moves to recovered, never available |
| Excess or duplicate recovery | Per-write-off uniqueness and caps reject it; account constraints backstop aggregate concurrency |
| Recovery reversal | New opposite legs restore charged-off money and principal capacity positions without rewriting history |
| Excess recovery reversal | Per-recovery uniqueness and caps reject it; no charged-off balance is inflated |
| Same ID with changed field | Automation quarantines the event instead of accepting it as idempotent |
| Ambiguous response | Exact lookup precedes any identical retry |
| Partial or mismatched lookup | Automation stops and opens a reconciliation incident |
| Shared query without ledger | Query is rejected; ledger-scoped queries succeed |
| Timeout above plan policy | Request is rejected before a TigerBeetle write |
| No PII | Ledger records and logs contain opaque references only |
Schema validation proves only that the request matches current accepted fields and ranges. It does not prove underwriting authorization, account existence, ledger consistency, provider finality, linked-chain correctness, balance sufficiency, or jurisdictional compliance.
Production operations
- Reconcile approved limit, available/drawn/over-limit/revoked/charged-off/recovered capacity, performing and charged-off principal/interest/fee receivables, unapplied cash, clearing accounts, provider events, servicing state, and general-ledger exports. Every edge must be traceable in both directions.
- Run draw, allocation, payment-return, write-off, and recovery workers from durable intent records. Use unique business-event and attempt-version constraints before allocating stable transfer IDs.
- Serialize state transitions that require a fresh allocation plan, especially payment returns, limit freezes, charge-offs, and facility closure. TigerBeetle constraints remain the concurrent backstop.
- Reconcile every pending draw promptly with a post or void. Alert before timeout and on natural expiry. Pending timeout is an in-flight safety valve, not a servicing scheduler.
- Version underwriting decisions, limits, contract terms, APR and day-count rules, fee policy, allocation order, rounding, delinquency, write-off, and recovery policy. Retain the exact versions used by every event.
- Treat clearing, accrual, assessment, charged-off receivable, recovered exposure, and suspense accounts as operational positions until finance maps them to the approved general ledger. Do not label a raw balance as recognized revenue, cash, recovery income, or credit loss without that mapping.
- Give every suspense item an owner, source evidence, reason, opened time, aging target, and approved resolution transfer. Alert on amount and age.
- Monitor create-result codes, ambiguous requests, pending age, provider lag, allocation lag, over-limit exposure, delinquency transitions, reconciliation differences, write-off approvals, recovery, plan quotas, and backup posture separately.
- Log sanitized facility, business-event, provider-event, database, ledger, account, transfer, policy-version, attempt-version, result-index, and result-code fields. Never log credentials, bank details, decision evidence, agreements, or unrestricted provider payloads.
- Keep the linked chain below both the public batch maximum and the active plan's event-per-request limit. Never split one invariant across requests to work around a limit.
- Rotate database-scoped keys with controlled overlap. Separate staging and production and narrow service responsibilities even when generated keys include both read and write scopes.
- Exercise ambiguous-response, provider/ledger split-failure, repayment-return-after-redraw, write-off, restore, and reconciliation incident procedures on an eligible non-production environment before launch.
- Use a production-eligible Parix topology and written support/SLA posture. Developer and Dedicated Single Node are not production plans or emergency production fallbacks.