ADR-014: DemozPay is a bank-to-bank orchestrator, not a custodian
- Status: Accepted
- Date: 2026-05-29
- Deciders: Principal Architect, Engineering Lead, Compliance/Legal (pending counter-sign)
- Relates to: ADR-001, ADR-005, ADR-006, ADR-009, ADR-012, ADR-013
- Supersedes: the implicit orchestrator-not-custodian guidance previously distributed across
CLAUDE.md,docs/architecture/BANK_ORCHESTRATION.md, the EWA + Lending domain comments, and theSYSTEM_GAP_ACTION_PLAN.mdrecommendation. This ADR makes the architectural commitment a single citable artefact.
Context
DemozPay moves money for Ethiopian employer-payroll-linked products: earned-wage access, salary-backed loans, BNPL (planned), savings/equb (planned). Every one of those flows could be implemented as a custodian — DemozPay takes the employer's funds into its own pooled bank account, holds them on behalf of employees, and disburses on request — OR as an orchestrator — funds always remain in partner-bank accounts (employer's payroll account, FI partner's pool account, BNPL underwriter's settlement account) and DemozPay only instructs partner banks to transfer money on a customer's behalf and records the obligation lifecycle in a ledger.
These two models look identical from a customer's UX but they are regulatorily, operationally, and architecturally distinct.
The custodian model — why we reject it
- Triggers Ethiopian National Bank of Ethiopia (NBE) licensing as a payment service provider or payment instrument issuer, requiring deposit-taking-equivalent capital adequacy, segregated trust accounts, regulatory reporting at the depository level, and supervisory examinations.
- Concentrates fraud + operational risk in DemozPay's own infrastructure. A single compromise of DemozPay's pool account is a multi-tenant loss.
- Requires balance-sheet management, treasury operations, float reconciliation, and per-customer interest accrual on held balances — none of which is a competitive surface for us.
- Creates a perpetual reputational obligation: customers reading "DemozPay holds my money" rationally expect deposit-insurance-equivalent protection. We cannot provide that.
The orchestrator model — why we choose it
- Stays inside the regulatory category we actually operate in: a technology platform facilitating bank-to-bank transfers on behalf of named originators. The NBE category map for this shape (e.g. fintech-as-a-technology-vendor with partner-bank settlement) is materially less burdensome than the custodian / PSP categories.
- Money custody belongs to partner banks (Dashen, CBE, Awash, Telebirr, future). They hold it; they bear the deposit-protection obligation; they are the entity the NBE supervises for custody risk.
- Operational simplicity — no treasury function, no float reconciliation, no per-customer interest accrual.
- Architectural simplicity — the ledger records obligations and movements, not custody positions. Three-account balance sheets (receivables, payables, revenue) replace twelve-account custodian balance sheets (cash, customer-funds-on-deposit, segregated-trust, interest-accrued-on-customer-funds, ...).
Decision
DemozPay is a bank-to-bank orchestrator. We do not, and will not, hold customer funds at rest.
This decision is binding and is enforced at every architectural layer:
1. Money custody boundary
| DemozPay | Partner bank | FI funder | |
|---|---|---|---|
| Holds customer money | NO | YES | YES (lending pool) |
| Authoritative balance | NO | YES | YES (per FI pool) |
| Initiates transfers | YES (via partner API) | NO (executes) | NO (funds the pool) |
| Records obligations + journal | YES (the ledger) | NO | NO |
| Reconciles against bank statements | YES | (publishes statements) | (may reconcile their side) |
| Bears settlement-failure risk | NO (passes through) | YES | (per contract) |
| Bears default risk (loan, BNPL) | NO | NO | YES (FI underwrites) |
2. Ledger taxonomy
The ledger records obligations and movements, never custody positions. Every account in every tenant's chart of accounts maps to ONE of:
- Receivables (ASSET) — what someone owes us at next payroll:
receivable-from-employee,receivable-from-borrower. - Payables (LIABILITY) — what we owe a partner:
payable-to-business-bank,payable-to-fi-partner[fi_id],payable-to-merchant[merchant_id](BNPL, planned). - Revenue (REVENUE) — fees and interest we recognise:
ewa-fee-revenue,loan-interest-revenue,loan-servicing-fee. - Clearing (ASSET) — short-lived intermediate accounts that exist between two settlement events:
payroll-clearing(employer deduction collected; not yet remitted to FI / business pool).
There is NO cash account. There is NO customer-funds-on-deposit account. There is NO segregated-trust account. Anywhere cash or equivalents appears in code or schema, it is a bug. GAP-04 removed the legacy cashAccountId from EWA and Lending adapters precisely to enforce this.
3. Storage invariant
ADR-006 already forbids stored balances. ADR-014 strengthens it: no Prisma column anywhere may represent a custody position. The legacy Wallet.balance (decimal) and LedgerAccount.balance (decimal) columns exist as architectural debt; they are @deprecated (Phase S4 / GL-12), ESLint-blocked from new imports, and scheduled for forward-migration deletion. The implicit semantic of those columns — "money DemozPay holds" — is the exact thing this ADR forbids.
4. Money-flow invariant
Every disbursement flow MUST trace from a partner-bank source account to a partner-bank destination account:
- EWA: employer's pre-funded payroll account at partner bank → employee's bank or wallet account. DemozPay never touches the cash.
- Lending: FI partner's pool account at FI's bank → employee's bank or wallet. DemozPay never touches the principal.
- BNPL (planned): BNPL underwriter's settlement account → merchant's bank account. DemozPay never touches the trade credit.
- Repayment (all products): employer's payroll deduction → employer's payroll account → FI / business pool → … . DemozPay never holds the deducted cash.
The ledger pre-commits a PENDING entry at instruction time and flips to POSTED on bank-confirmation (ConfirmSettlement) — but the money's location is recorded by the bank, not by us. See docs/architecture/BANK_ORCHESTRATION.md for the operational detail and the per-flow trace.
5. Truth ranking
When the ledger and a partner-bank statement disagree, the bank wins. The reconciliation primitive (Ledger.ReconcileWithBank + the daily recon-runner shipped in Phase D) is structured around this — it computes drift = ledger_total - statement_total and treats any non-zero result as a finding for the engineering / finance team to investigate, not as authority to overwrite the bank's number. See docs/architecture/RECONCILIATION_ARCHITECTURE.md §1.
Alternatives considered
A — Operate as a custodian (full PSP licensing)
Rejected. Custody triggers NBE PSP-tier capital, segregated-trust, deposit-protection, and ongoing supervisory cost. We have no competitive reason to take that on.
B — Hybrid: custody for in-flight settlement only ("pass-through wallet")
Rejected. Even momentary custody — money in DemozPay's account for hours during a payroll cycle — pulls the entire regulatory category in. Pass-through trust accounts are still trust accounts. "Hybrid" is an architectural euphemism for "custodian with extra steps".
C — Per-tenant DemozPay-owned bank accounts (white-label custody)
Rejected. Same regulatory shape as A, with additional operational complexity from per-tenant treasury management. White-label custody is custody.
D — Reject this ADR; let the model float
Rejected. Without an explicit position, individual code reviews drift toward custody patterns whenever they're convenient — first a "clearing" account that's used as a real holding pool, then a "settlement buffer" that holds money for a week, then a "float reserve". Architecture decisions that aren't written down are not decisions; they are accidents waiting to happen. ADR-014 prevents the drift.
Consequences
Positive
- Regulatory clarity. First NBE conversation can cite a single artefact: "We do not custody. Custody is at Dashen / CBE / partner FIs. We are a technology platform that orchestrates and reconciles." Material reduction in licensing burden, examination scope, and ongoing reporting cost.
- Partner-bank security review surface shrinks. "Do you hold customer funds? No, you do. Here is the ledger taxonomy proving it. Here is the recon primitive proving we reconcile against your statements daily." Closes the most invasive question of the typical due-diligence questionnaire.
- Architectural enforceability. The decision is visible at every layer: ledger schema (no
cashaccount), use-case code (pre-commit PENDING → bank confirms → POSTED), reconciliation (bank statement is truth), audit (every state change recorded in the same DB transaction). A future engineer who tries to add custody must violate multiple existing invariants — they will be caught at review. - Operational simplicity. No treasury function. No float management. No per-customer interest accrual. No deposit-protection obligation. The entire balance-sheet shape is receivables + payables + revenue; we do not run a balance sheet around customer funds.
- Carve-out cleanliness. Each domain (EWA, Lending, BNPL, …) can be extracted to its own deployable without renegotiating "who holds the money" — the answer was always "the partner bank".
Negative
- Partner dependency is structural. Without Dashen or an equivalent active, the platform cannot transfer money. The single biggest external risk surface lives here. Mitigated by per-partner adapter abstraction (
services/integration-gateway/internal/adapters/), the Phase C LookupAccount safety gate, and a multi-partner roadmap (Dashen first, CBE / Awash / Telebirr / M-Birr planned). - Settlement timing surfaces as a customer-facing concern. Because DemozPay does not pre-position cash, every disbursement is bank-side-async. We must model SUBMITTED_TO_BANK / ACCEPTED_BY_BANK / SETTLED states in domain enums and explain "your money is on its way" to users honestly. This is already done (GAP-03 / Phase A).
- Some products require pre-funding by partners. EWA needs the employer's payroll account pre-funded with float; lending needs the FI partner's pool pre-funded with capital. We must negotiate these arrangements per partner; we cannot solve them with our own balance sheet.
- No interest income on customer funds. A custodian earns float interest on held customer balances. We cannot. Revenue model is entirely fee + interest spread on partner-funded lending.
Enforcement mechanisms
The decision is enforced at multiple layers so a single missed review does not introduce custody:
| Layer | Mechanism |
|---|---|
| Ledger schema | No cash / customer-funds / trust-account table. Account-type enum (ASSET / LIABILITY / EQUITY / REVENUE / EXPENSE) makes adding a custody account architecturally visible. |
| Domain code | Ledger-accounts adapters (apps/api/src/products/ewa/ledger-accounts.adapter.ts, apps/api/src/products/lending/ledger-accounts.adapter.ts) expose ONLY the obligation / receivable / payable / clearing taxonomy. GAP-04 removed the cashAccountId field. |
| Code review | This ADR is the citable reference for "this PR is introducing custody — close it." |
| ESLint | The deprecated Wallet* / LedgerAccount.balance columns are import-blocked outside apps/api/prisma/ (GL-12 / Phase S4). |
| Migration policy | A migration adding a custody account requires an ADR amendment to this one. No silent custody. |
| Audit + reconciliation | Daily recon-runner (Phase D) emits drift summary. Persistent non-zero drift would surface custody-shape accounting errors. |
What this commits us to (and what it does NOT)
This ADR commits us to
- Never introducing a Prisma model or ledger account that represents money DemozPay owns or holds.
- Designing every new product (BNPL, savings, equb) around partner-bank custody from the first ADR forward.
- Citing partner-bank statements as the authoritative source of money truth in every reconciliation.
- Documenting partner relationships, exposure limits, and settlement SLAs as binding operational dependencies, not implementation details.
This ADR does NOT commit us to
- A specific partner. Dashen is the first integration; CBE, Awash, Telebirr, M-Birr are planned. The orchestrator model is partner-agnostic.
- A specific regulatory licensing category beyond "not a custodian / PSP". The exact NBE designation we operate under is determined by ongoing legal / compliance work; this ADR only narrows the universe of possible categories.
- A specific fee model. Revenue can be subscription, per-transaction, interest spread, or a mix.
- A timeline for BNPL / Savings / Equb. Those domains are
PlannedperSYSTEM_NOT_IMPLEMENTED.md; this ADR governs HOW we'd build them, not WHEN.
Follow-ups
- Compliance / Legal counter-sign. This ADR is technically accepted by Engineering; final regulatory positioning needs Legal counter-sign before external use. Status: pending.
- Partner-contract template alignment. Every partner agreement (Dashen, future FIs, future merchants) MUST reflect "DemozPay does not custody; partner is custodian; DemozPay reconciles daily against partner statements". Track the template revisions outside the codebase.
- Per-product chart-of-accounts review. When each new domain ships (BNPL, savings, equb), re-confirm via design review that its proposed account taxonomy contains ZERO custody accounts.
- Cross-reference this ADR from CLAUDE.md and SECURITY_CONTROLS.md. Both currently imply the orchestrator stance without naming the citable artefact. Update after this ADR lands.
- Delete legacy custody-shaped schema.
Wallet,WalletTransaction,WithdrawalRequestPrisma tables are@deprecated. After Phase E completes, ship a forward migration that drops them. Reduces the architectural attractive-nuisance surface.
Relationship to other ADRs
This ADR is the regulatory + business-model layer that the technical ADRs (006 ledger truth, 009 no-deletes, 012 ledger accounting model, 013 tenant isolation) collectively implement. Without ADR-014, those technical ADRs are coherent engineering choices but lack a single statement of why they are mandatory. With ADR-014, they become the architectural enforcement of the orchestrator model.
The relationship:
ADR-014: orchestrator, not custodian (regulatory + business decision)
│
├─ enforced by ADR-006 (ledger sole source of truth — no stored balances)
├─ enforced by ADR-009 (no deletes — append-only ledger as audit trail)
├─ enforced by ADR-012 (double-entry accounting model — DB-level invariants)
├─ enforced by ADR-013 (tenant isolation — no cross-tenant custody leakage)
└─ enabled by ADR-005 (santim NUMERIC(20,0) — exact-integer money everywhere)
This ordering matters: a future ADR that proposed weakening any of 005/006/009/012/013 would also be implicitly weakening ADR-014, and must be evaluated against this ADR's commitment.
Citations and supporting evidence
For external audiences (regulators, partner-bank security reviews, investor diligence) the supporting evidence chain:
- Code-level enforcement —
packages/ewa/backend/application/ports/ledger-accounts.port.tsandpackages/lending/backend/application/ports/ledger-accounts.port.tsshow the production account taxonomy. Zerocashaccounts. - Schema-level enforcement —
services/ledger/migrations/0001_init.up.sqldefines the account-type enum + balance-derivation view. No stored balance column. - Reconciliation evidence —
Ledger.ReconcileWithBankRPC + Phase Drecon-runnerproduce daily signed-drift evidence per (tenant, partner) — proof that we reconcile against the partner's authoritative statement, not against our own books. - Money-flow trace —
docs/architecture/MONEY_FLOWS.mddocuments the 12 canonical flows, each one annotated with "money location" at every step. Every flow originates and terminates at a partner-bank account.
Decision log
- 2026-05-29 — Accepted by Engineering. Pending Legal / Compliance counter-sign.