Part 18 · 16 chapters · ~20 min

Round sixteen: “who is this customer, and what may they do?”

Every round so far has treated an account as an account. Real banks do not: a tier-1 wallet and a corporate current account are different objects with different limits, different permissions and different rules about what may be taken from them and by whom. This round builds the entitlement layer, and then spends half its length on the most misunderstood mechanism in Nigerian banking, the post-no-debit.

199

The pressure: not every account is the same account

interviewer

“A customer signs up with just a phone number. Another uploads a BVN and a utility bill. A third is a registered company with three signatories. All of them have accounts. What is different?”

Two independent dimensions, and conflating them is the most common modelling error in this area.

worked numbers
KYC tier    how much we know about who they are

                        → decides limits and permitted activity

                        → a property of the customer


account type  what the account is for

                        → decides features, interest, charges, mandates

                        → a property of the account


one customer, one tier, many accounts, several types.

modelling tier on the account is the mistake that produces

a customer who is tier 3 on one account and tier 1 on another.
functional, new
  1. Every customer has a KYC tier, and every account has a type.
  2. Limits are resolved from both, deterministically, with the stricter winning.
  3. A tier can be upgraded, and rarely downgraded, with the effect on existing balances defined.
  4. Business accounts support mandates: who may instruct, and how many are required.
  5. An account can be placed on post-no-debit while still receiving credits.
  6. A service can register a claim on future credits, and have it satisfied automatically.
non-functional, new
  1. Limit resolution adds under 3 ms to the posting path.
  2. A restriction takes effect within 2 seconds of being applied, everywhere.
  3. A credit that satisfies a claim is applied within 5 seconds of landing.
  4. No restriction may ever cause the ledger to disagree with itself.
200

KYC tiers: what each tier buys the customer

Tiering exists because regulators want financial inclusion and anti-money-laundering controls at the same time. The compromise is graduated: less identity, less capability.

TierRequiresSingle txnDailyMax balance
Tier 1
minimal
Phone number, name, photo. No document verification₦50,000₦50,000₦300,000
Tier 2
simplified
+ BVN, address, valid ID₦200,000₦500,000₦500,000
Tier 3
full
+ verified address, reference, full documentationNo capNo capNo cap
the constraint engineers usually miss
The maximum balance is a cap on a credit, not on a debit. If a tier-1 customer holds ₦280,000 and someone sends them ₦100,000, the credit would breach the cap. You cannot simply reject it, because the money has already left the sender. The answer is to accept it into a suspense account, notify the customer that an upgrade is required to access it, and either release on upgrade or return it after a defined period. This is the detail that separates someone who has implemented tiering from someone who has read about it.
tier-1 customer receives ₦100,000 that would breach their cap · journal kind: inbound.tier_capped
bank_account:gtb_main−10000000the cash is real and has arrived
suspense:tier_cap_held+10000000held pending upgrade or return
Σ0the invariant holds; the customer is told what to do
201

Account types: personal, business, savings, current

TypeInterestOverdraftWithdrawalsDistinctive rule
SavingsYes, credit interestNoLimited free per monthExceeding the free count forfeits that period's interest in many banks
CurrentUsually noneYesUnlimitedAttracts account maintenance charges
WalletNoneNoUnlimitedOften tier-capped, and not a bank account in the legal sense
Fixed depositHigher, fixed termNoLockedEarly withdrawal forfeits accrued interest
DomiciliaryLow or noneRarelyFX rules applyForeign currency, with its own regulatory reporting
Corporate currentNoneYes, largerUnlimitedMandates and signatories. Chapter 205
Escrow / trustVariesNoConditionalFunds released only on a defined condition
how this maps onto what we already built
An account type is a bundle of policy, not a new kind of ledger object. Interest is the Part 5 accrual batch with a different rate. Overdraft is the Part 5 facility, present or absent. A withdrawal limit is a Part 5 velocity counter. A fixed deposit lock is a Part 5 hold with a maturity date. Nothing in the ledger changes; the entitlement layer above it gains a lookup.
code
-- the account type is a reference to POLICY, not a column of booleans.
-- adding a product later must not mean an ALTER TABLE.
CREATE TABLE account_types (
  code               TEXT PRIMARY KEY,   -- savings | current | wallet | fd …
  display_name       TEXT NOT NULL,
  -- capabilities, as data
  earns_interest     BOOLEAN NOT NULL,
  interest_bps       INT,
  allows_overdraft   BOOLEAN NOT NULL,
  free_withdrawals   INT,                -- null = unlimited
  maintenance_fee_minor BIGINT,
  requires_mandate   BOOLEAN NOT NULL DEFAULT FALSE,
  -- which KYC tiers may hold this type at all
  min_kyc_tier       INT NOT NULL DEFAULT 1,
  effective_from     TIMESTAMPTZ NOT NULL,
  effective_to       TIMESTAMPTZ
);
202

The tier × type matrix, and why it is data

Three tiers and seven types is twenty-one combinations, and not all of them are legal. Encoding that in conditionals is how a codebase becomes unmaintainable.

WalletSavingsCurrentCorporateDomiciliary
Tier 1✓ capped✓ capped✗✗✗
Tier 2✓ capped✓ capped✓✗✓ limited
Tier 3✓✓✓✓ if incorporated✓
why this must be data rather than code
  1. Regulators change it. A circular can move a tier-1 cap overnight, and it must not need a release.
  2. It is time-varying. A dispute about a 2024 transaction must be evaluated against the 2024 matrix, exactly like Part 7's compliance policies.
  3. Compliance staff own it, and they should be able to read and prepare changes without engineering.
  4. It is auditable. "Why was this allowed?" resolves to a specific row with a specific validity period.
code
CREATE TABLE tier_type_policy (
  kyc_tier       INT NOT NULL,
  account_type   TEXT NOT NULL REFERENCES account_types(code),
  permitted      BOOLEAN NOT NULL,
  -- nulls mean "no limit from this dimension"
  max_single_minor     BIGINT,
  max_daily_minor      BIGINT,
  max_monthly_minor    BIGINT,
  max_balance_minor    BIGINT,
  -- which transaction kinds this combination may perform at all
  permitted_kinds TEXT[],
  -- the regulatory instrument this row implements, for audit
  source_ref     TEXT,
  effective_from TIMESTAMPTZ NOT NULL,
  effective_to   TIMESTAMPTZ,
  PRIMARY KEY (kyc_tier, account_type, effective_from)
);
the pattern, for the fourth time
Compliance rules in Part 7, fraud rules in Part 9, and now the tier matrix: all three are versioned, effective-dated data evaluated by one engine. That is not a coincidence. Anything that changes on somebody else's timetable rather than your release schedule belongs in a table with validity periods, and recognising that class of requirement is more valuable than any individual implementation.
203

Resolving limits when both apply

A tier-2 customer with a current account has limits from the tier, from the type, from their own self-imposed settings, and possibly from a fraud restriction. They must resolve to one number, deterministically.

code
// the rule: the MOST RESTRICTIVE wins, always, and we record WHICH.
// "declined" is unhelpful. "declined: your tier-2 daily limit" is actionable.
interface ResolvedLimit {
  effectiveMinor: bigint;
  bindingSource: 'kyc_tier' | 'account_type' | 'customer' | 'risk';
  allSources: { source: string; limitMinor: bigint | null }[];
}

function resolveLimit(ctx: Ctx, dim: LimitDimension): ResolvedLimit {
  const candidates = [
    { source: 'kyc_tier',     limitMinor: ctx.tierPolicy[dim] },
    { source: 'account_type', limitMinor: ctx.typePolicy[dim] },
    { source: 'customer',     limitMinor: ctx.customerPrefs[dim] },
    { source: 'risk',         limitMinor: ctx.riskRestriction?.[dim] }
  ].filter(c => c.limitMinor != null);

  if (!candidates.length) return { effectiveMinor: UNLIMITED, ... };

  // minimum, not the last one set, not a precedence order.
  const binding = candidates.reduce((a, b) =>
    b.limitMinor! < a.limitMinor! ? b : a);

  return { effectiveMinor: binding.limitMinor!,
           bindingSource: binding.source as any, allSources: candidates };
}
two rules that prevent most of the bugs here
One: the minimum wins, never a precedence order. A precedence order means a customer's own self-imposed ₦10,000 limit could be overridden by a tier-3 unlimited policy, which is exactly backwards. Two: a customer may always lower their own limit and never raise it above the resolved maximum, which is the same "request a restriction, never an expansion" rule from Part 14 chapter 165.
limit resolution
four sources, one effective number
swipe the figure sideways, or tap expand for full screen
1/7
four opinions
A customer wants to send money. Four different things have an opinion about how much they may send in a day.
204

Tier upgrades, downgrades, and grandfathering

upgrade, which is the easy direction
  1. Documents submitted and verified, and the tier changes.
  2. Limits widen immediately, which needs no special handling.
  3. Anything held in tier-cap suspense is released to the customer's account.
  4. The change is event-sourced, so the limit cache and the card authoriser both learn within seconds.
tier upgrade releases previously capped funds · journal kind: tier.cap_released
suspense:tier_cap_held−10000000suspense clears
wallet A+10000000customer can finally spend it
Σ0the money was never lost, only parked
downgrade, which is genuinely hard
  1. Triggered by document expiry, a failed re-verification, or a regulatory instruction.
  2. The customer may already hold more than the new tier's cap allows.
  3. You cannot confiscate the excess, and you cannot let them exceed the cap indefinitely.
  4. The resolution is grandfathering with a runway: the balance is permitted, new credits are capped, and the customer is given a defined period to re-verify.
  5. Only at the end of that period does the account become credit-restricted, which is the Part 9 state doing useful work.
the principle to state
A rule change must never make an existing lawful balance unlawful retroactively. Grandfather the position, restrict the flow, and give a runway. The same logic applies when a regulator lowers a cap: existing holders are permitted their current balance while new credits are constrained, because the alternative is telling millions of customers their money is now inaccessible through no action of their own.
205

Business accounts: mandates, signatories, dual control

A corporate account is not owned by a person, so "is this the account holder?" is the wrong question. The right one is "is this instruction properly authorised under the mandate?"

code
CREATE TABLE mandates (
  id            UUID PRIMARY KEY,
  account_id    UUID NOT NULL,
  -- "any one", "any two", "A and either B or C", or by amount band
  rule_type     TEXT NOT NULL,   -- any_one | any_n | specific_combination
  required_count INT,
  -- larger amounts need more signatures. the common real-world case.
  threshold_minor BIGINT,          -- this rule applies above this amount
  effective_from TIMESTAMPTZ NOT NULL,
  effective_to   TIMESTAMPTZ
);

CREATE TABLE signatories (
  mandate_id  UUID NOT NULL REFERENCES mandates(id),
  customer_id UUID NOT NULL,
  -- class A and class B signatories, for "one of each" rules
  signatory_class TEXT,
  can_initiate BOOLEAN NOT NULL DEFAULT TRUE,
  can_approve  BOOLEAN NOT NULL DEFAULT TRUE,
  PRIMARY KEY (mandate_id, customer_id)
);
worked numbers
          a mandate turns one instruction into a multi-step approval flow:


            initiated by signatory A      → state: pending_approval

            approved by signatory B     → state: authorised

            posted                             → state: completed


the funds are HELD at initiation, not at approval.

          otherwise the balance can be spent while an approval is pending,

and the approved instruction then fails for insufficient funds.
the detail worth raising
Place the hold when the instruction is initiated, not when it is approved. Otherwise a second signatory approves a payment that can no longer be funded, because someone spent the money in between. This is the Part 5 hold doing exactly what it was built for, applied to authorisation rather than to a card, and it is the kind of reuse that shows the model was right.
what else corporate accounts need
  1. Initiator cannot self-approve. A signatory who can do both must still not satisfy a two-signature mandate alone.
  2. Mandate changes are themselves mandated, usually requiring more signatures than a payment.
  3. Bulk payment files, where one approval covers hundreds of lines, with the file hash as the thing approved.
  4. A visible audit trail, because corporate customers are audited too and will ask for it.
206

PND: post-no-debit, and its four varieties

The instruction that stops money leaving while allowing it to arrive. Part 9 introduced debit_restricted as a state; here is what it actually means operationally, because "PND" covers four different things.

Full PNDthe classic No debit of any kind may leave the account. Credits land normally. Typically a regulatory or court instruction, or a confirmed fraud case. all debits
blocked
Partial PNDamount-bounded A specific amount is frozen; the rest of the balance is spendable. A court ordering ₦2m frozen on an account holding ₦5m leaves ₦3m usable. This is a lien, and it is the Part 5 hold. ₦X frozen
rest free
Selective PNDby channel or kind Outbound transfers blocked while card payments continue, or the reverse. Used when the compromise is channel-specific, such as a stolen card with an uncompromised app login. by transaction
kind
Soft PNDreview, not block Debits are allowed but routed to manual review before posting. Slower, and it keeps a legitimate customer functioning while a case is investigated. queued for
review
code
-- PND is a restriction with a DIRECTION and a SCOPE, extending the
-- Part 9 restriction model rather than replacing it.
ALTER TABLE account_restrictions
  ADD COLUMN direction TEXT NOT NULL DEFAULT 'debit',  -- debit|credit|both
  ADD COLUMN amount_minor BIGINT,        -- null = the whole balance
  ADD COLUMN scoped_kinds TEXT[],        -- null = all kinds
  ADD COLUMN mode TEXT NOT NULL DEFAULT 'block';   -- block | review

-- and the check on the posting path is direction-aware:
--   a CREDIT entry is unaffected by a debit-direction restriction.
--   a DEBIT entry is refused, or queued, per mode.
why the four varieties matter
Treating every PND as a full freeze is operationally lazy and causes real harm. A customer under investigation who cannot pay rent because we froze everything, when a partial lien on the disputed amount would have sufficed, is a complaint and possibly a regulatory finding. Use the narrowest restriction that achieves the purpose, which is the proportionality rule from Part 9 chapter 107, stated again because it is repeatedly ignored.
207

Receiving while restricted: credit-in, debit-out

The asymmetry that makes PND useful, and the one that surprises people implementing it for the first time.

worked numbers
          account on full PND, balance ₦40,000


            salary of ₦300,000 arrives        → ACCEPTED

            balance becomes ₦340,000

            customer attempts to send ₦5,000 → REFUSED


the account grows and nothing can leave.


          why accept at all?

            · the sender has already parted with the money

            · rejecting creates a return leg and a second reconciliation problem

            · for a fraud case, letting funds accumulate is often the point:

              it preserves them for recovery to victims

            · the customer's obligations, such as a salary arriving, are not

              the thing being restricted
        
the exception: credit-restricted, which is rarer and narrower
  1. Applied when the account is a suspected mule collection point, so every incoming credit is a further victim.
  2. The incoming payment is returned to the sender rather than refused silently, which requires a return leg on the originating rail.
  3. Almost never combined with debit restriction on a live customer, because a fully frozen account with returned credits strands a person completely.
  4. Usually accompanies a closure process rather than an investigation.
code
// the check, direction-aware, on the posting path. note it inspects
// the SIGN of the entry rather than the operation's name, so a fee
// leg and a transfer leg are treated identically and correctly.
function checkRestrictions(e: EntrySpec, r: Restriction[]): Decision {
  const isDebit = e.amountMinor < 0n;

  for (const x of r) {
    if (x.direction === 'credit' && isDebit) continue;   // not our concern
    if (x.direction === 'debit' && !isDebit) continue;   // credits pass a PND
    if (x.scopedKinds && !x.scopedKinds.includes(e.kind)) continue;

    return x.mode === 'review'
      ? { action: 'queue_for_review', restrictionId: x.id }
      : { action: 'refuse', reasonCode: x.reasonCode, restrictionId: x.id };
  }
  return { action: 'allow' };
}
208

Auto-debit on credit: the standing sweep instruction

A service needs money from an account that does not currently have it. Rather than polling, it registers an instruction that fires the moment a credit lands. Part 14 built this for loan sweeps; here it is generalised.

ApproachLatencyCostVerdict
Poll the balancePolling intervalEnormous. 20M accounts × frequencyRejected. It does not scale and it is always late
Scheduled retryHoursLowUseful as a backstop, useless as a primary
Event-driven on creditSecondsProportional to actual creditsChosen. The Part 4 event log already carries every credit
Synchronous hook in the posting pathImmediateAdds latency to every creditRejected. It couples an unrelated service to the money path
code
CREATE TABLE credit_claims (
  id            UUID PRIMARY KEY,
  account_id    UUID NOT NULL,
  claimant      TEXT NOT NULL,      -- which service registered it
  amount_minor  BIGINT NOT NULL,    -- what is owed
  collected_minor BIGINT NOT NULL DEFAULT 0,
  -- lower number = collected first. statutory claims outrank commercial ones.
  priority      INT NOT NULL,
  -- never take the customer below this. the line between recovery and harm.
  protected_floor_minor BIGINT NOT NULL DEFAULT 0,
  -- partial collection allowed, or all-or-nothing?
  allow_partial BOOLEAN NOT NULL DEFAULT TRUE,
  -- the customer agreed to this, and when
  consent_ref   TEXT NOT NULL,
  expires_at    TIMESTAMPTZ,
  state         TEXT NOT NULL       -- active | satisfied | cancelled | expired
);

CREATE INDEX claims_active ON credit_claims (account_id, priority)
  WHERE state = 'active';
the consent column is not optional
Automatically taking money from an account requires a recorded, referenceable consent, whether that is a loan agreement, a direct debit mandate or a court order. A claim with no consent_ref should be impossible to create. This is one of those cases where the schema encodes a legal requirement, and making the column NOT NULL is the cheapest compliance control you will ever ship.
auto-debit on credit
a claim waiting on an empty account
swipe the figure sideways, or tap expand for full screen
1/8
empty account
An account with a zero balance. Several services need money from it and there is none.
209

Posting ahead: claims against future credits

The variant you described: a service posts ahead, declaring it needs an amount before the money exists. There are two ways to implement it, and they have very different accounting consequences.

Claim registryNegative posting
MechanismA row in credit_claims. No entries writtenPost the debit immediately, letting the balance go negative
Ledger effect before funds arriveNone. The invariant is untouchedThe account is overdrawn, so the bank has lent money
Shows in the balanceOnly as reduced available balanceAs a negative ledger balance
AccountingNothing to recognise until collectedA receivable exists, and may need provisioning
RegulatoryCleanAn unauthorised overdraft unless a facility exists
VerdictDefault. Use unless there is a reason not toOnly with an explicit overdraft facility, per Part 5
the distinction that matters
A claim is an intention to collect. A negative balance is a loan. They feel similar to an engineer and are completely different to an accountant and a regulator. Posting a debit that drives an account negative without a facility creates unauthorised credit exposure, which appears in capital calculations and can be a regulatory breach. The claim registry keeps the intention out of the ledger until it can be honoured, and that is why it is the default.
the WRONG way: posting ahead into a negative balance with no facility · creates an unauthorised loan
wallet A (balance 0)−500000balance is now −₦5,000
service_receivable+500000the service has been paid from money that does not exist
Σ0the invariant holds and the bank has lent ₦5,000 it never agreed to
when a negative posting is legitimate
  1. An overdraft facility exists and this transaction kind may draw it, per Part 5 chapter 58.
  2. A card clearing arrives for more than was authorised, which we must post regardless, per Part 8 chapter 92.
  3. A reversal of a credit that has already been spent, which is the ACH return case from Part 6 chapter 71.
  4. In all three, the negative balance is a known, accounted-for exposure rather than an accident of ordering.
210

Ordering when several claims compete for one credit

₦50,000 arrives against ₦180,000 of claims from four different services. Who gets paid, and in what order? This is a policy question that must be encoded, because the alternative is whichever consumer happened to process the event first.

worked numbers
          credit: ₦50,000   protected floor: ₦5,000

          allocatable: ₦45,000


p10 court-ordered garnishment  ₦30,000  → ₦30,000 collected

p20 loan repayment due          ₦80,000  → ₦15,000 partial

p30 card repayment              ₦40,000  → nothing

p40 subscription                 ₦30,000  → nothing


          customer keeps: ₦5,000
the priority bands, and why they are ordered so
  1. p0-p9 statutory. Court orders, tax garnishment. Legally must be first, and no commercial claim may outrank them.
  2. p10-p19 secured. Claims against specific pledged funds.
  3. p20-p29 lending. Loan and overdraft repayment, where non-payment damages the customer's credit standing.
  4. p30-p39 bank fees. Our own charges. Deliberately below lending, because taking our fee before their loan repayment worsens their position and ours.
  5. p40+ discretionary. Subscriptions, savings plans, anything the customer opted into for convenience.
code
// allocation must be ATOMIC across all claims, or two consumers
// processing the same credit event could each allocate the full amount.
await db.transaction(async tx => {
  // one idempotency key per credit event, so a redelivery is a no-op
  const key = `claims-${creditEntryId}`;

  const claims = await tx.query(`
    SELECT * FROM credit_claims
     WHERE account_id = $1 AND state = 'active'
     ORDER BY priority, id
     FOR UPDATE                      -- lock them all, in a stable order
  `, [accountId]);

  let available = creditAmount - protectedFloor;
  const entries: EntrySpec[] = [];

  for (const c of claims) {
    if (available <= 0n) break;
    const outstanding = c.amount_minor - c.collected_minor;
    const take = c.allow_partial ? min(available, outstanding)
                                : (available >= outstanding ? outstanding : 0n);
    if (take === 0n) continue;
    entries.push(...claimEntries(c, take));
    available -= take;
  }

  if (entries.length) await ledger.post({ idempotencyKey: key, entries });
});
the ordering detail that prevents deadlocks
ORDER BY priority, id FOR UPDATE locks claims in a stable, total order. Without the id tiebreaker, two credits arriving simultaneously on accounts sharing claims could acquire locks in different orders and deadlock, which is exactly the Part 2 chapter 27 problem appearing in a new place.
211

Backlog clearing, and why it is not a queue

When a restriction is lifted, or funds finally arrive, there is a backlog of things that could not happen. Processing it naively causes a second incident.

why a plain FIFO queue is the wrong model
  1. Some items expired. A card authorisation held for three days should not suddenly execute when the PND lifts.
  2. Some items are now invalid. The beneficiary account may have closed, or the customer may have cancelled.
  3. Order by arrival is not order by priority. A statutory garnishment queued after a subscription must still be collected first.
  4. The funds may not cover everything, so it is an allocation problem rather than a replay.
  5. Replaying all at once creates a thundering herd against the ledger, and against the notification system, which then texts the customer forty times.
worked numbers
wrong: on restriction lift → replay the queue in arrival order


right: on restriction lift →

            1. expire everything past its validity

            2. revalidate the rest against current state

            3. re-prioritise, because arrival order is not priority order

            4. allocate against the actual available balance

            5. rate limit execution, so the ledger and the customer survive it

            6. summarise notifications into one digest, per Part 12
        
the customer-facing consequence
A customer whose PND lifts at 09:00 should receive one notification saying what happened, not forty individual alerts as a three-day backlog drains. That is the Part 12 digest mechanism, and backlog clearing is its most important use case, because it is the moment when the volume is guaranteed to spike for a single customer.
backlog clearing as a first-class operation
  1. It has its own state machine: pending → validating → allocating → executing → complete.
  2. It is idempotent, keyed on the restriction-lift event, so a retry does not double-execute.
  3. It is observable: backlog depth and age per account are metrics, because a backlog that never clears is a stuck customer.
  4. It is interruptible, because a new restriction may be applied mid-clear and must take effect immediately.
212

Restriction precedence: who wins when rules conflict

An account can simultaneously carry a court lien, a fraud hold, a tier cap, a customer-requested freeze and an expired document flag. They must resolve deterministically.

LevelSourceMay be lifted byOverrides
1Court order / regulatorThe issuing authority onlyEverything. No internal override exists
2Sanctions matchCompliance, after clearingEverything below
3Confirmed fraudFraud analyst, four-eyesEverything below
4Automated riskExpires automatically, or an analystBelow only
5KYC / document expiryThe customer, by re-verifyingBelow only
6Customer self-imposedThe customer, with a cooling-off periodNothing
code
// the resolution rule: ANY blocking restriction blocks. precedence
// decides who may LIFT it, not whether it applies.
function effectiveRestriction(all: Restriction[]): Effective {
  const active = all.filter(isActive);
  if (!active.length) return { blocked: false };

  // the MOST restrictive applies; the LOWEST level number decides
  // who is allowed to remove it.
  const governing = active.reduce((a, b) => b.level < a.level ? b : a);

  return {
    blocked: true,
    reasonCode: governing.reasonCode,
    liftableBy: governing.liftableBy,
    // ALL of them, because lifting one does not unblock the account
    // and support needs to see the full picture.
    allActive: active
  };
}
the support-facing consequence
An agent who lifts a fraud hold and sees the account still frozen will escalate, reopen, and waste an hour, unless the tool shows every active restriction at once and states which ones they are authorised to remove. Returning only the governing restriction is technically correct and operationally useless.
213

Support tooling and the four-eyes boundary

Everything in this part creates support actions, and support tooling is the largest insider-risk surface in a bank, because its whole purpose is to let staff change things about customer money.

ActionWhoControl
View account state and historyAny agentLogged, and rate-limited per agent to detect bulk browsing
Apply a restrictionAgentLogged with a reason code. Restricting is low risk
Lift a restrictionSenior agentFour-eyes above a threshold. This is the dangerous direction
Raise a limitSupervisorFour-eyes, time-boxed, and it expires automatically
Cancel a claimSupervisorFour-eyes, because it forgives money owed
Post a manual adjustmentFinance onlyFour-eyes always, capped in value, reconciled daily
Upgrade a KYC tierComplianceRequires document evidence attached to the action
the asymmetry that should shape every tool
Restricting is safe; un-restricting is dangerous. A wrongly applied restriction is an inconvenience that is quickly reversed. A wrongly lifted one releases money that does not come back. So the controls are deliberately asymmetric: one agent may freeze, two are required to unfreeze. Designing permissions around the direction of risk rather than around job titles is the move that matters here.
what every support action must record
  1. Who, by verified identity rather than a shared login.
  2. What, as a structured action rather than free text.
  3. Why, as a reason code plus a ticket reference.
  4. Approved by whom, where four-eyes applied.
  5. Into the append-only audit log from Part 13, which is itself monitored for anomalies by Part 9.
214

Sketch v16: the entitlement layer

What changed, and the cost accepted

ChangeDriven byCost accepted
Tier on the customer, type on the accountThey are genuinely independent dimensionsTwo lookups on the posting path, cached
Tier × type matrix as versioned dataRegulators change it without noticeA policy store to govern, like Part 7's
Limits resolve to the minimumA precedence order lets a wide policy override a narrow oneMust report the binding source, or declines are unactionable
Tier-cap suspense accountA credit that breaches a cap cannot simply be refusedAnother suspense balance to age and monitor
PND with direction, scope and modeFour varieties, and a full freeze is usually disproportionateRestriction checks become direction-aware on every entry
Claims registry, not negative postingA negative balance without a facility is an unauthorised loanClaims must be allocated atomically on every credit
Priority bands with a protected floorStatutory claims outrank commercial ones, and customers must eatA policy decision that has to be owned by someone senior
Backlog clearing as a state machineReplaying a queue causes a second incidentExpiry, revalidation, re-prioritisation and rate limiting
Asymmetric four-eyesLifting a restriction is far riskier than applying oneSlower support resolution on the lifting path, deliberately
how to close round sixteen
"v16 adds an entitlement layer between callers and the ledger, and the ledger itself still has not changed. Three things I would defend: tier belongs to the customer and type belongs to the account, because modelling tier on the account produces a customer who is two different tiers at once; a credit that breaches a tier cap goes to suspense rather than being refused, because the sender has already parted with the money; and a claim on a future credit is not a negative posting, because a negative balance without a facility is an unauthorised loan with capital consequences. What is still missing is that nobody has explained where the bank's own money comes from."
architecture v16
entitlement sits between the caller and the ledger
swipe the figure sideways, or tap expand for full screen
1/8
callers and ledger
Callers on the left, the ledger on the right, exactly as before.