Part 8 · 11 chapters · ~20 min

Round eight: “issue cards”

Every transaction so far started with our customer, in our app, on our terms. A card inverts that: a terminal in a shop somewhere sends us a debit request, and we have under two seconds to answer, whether or not our systems are healthy. This round is about designing for a latency budget we do not own and a failure mode where saying nothing is worse than saying no.

88

The pressure: someone else initiates the debit

interviewer

“We are issuing debit cards on Verve and Mastercard. A terminal in a supermarket sends an authorisation request. You have two seconds. What happens, and what happens if your ledger is unreachable?”

Three things are genuinely new, and naming them establishes why this round needs different machinery.

what inverts
  1. We are no longer the initiator. The request arrives from outside, on the network's schedule, at the network's volume.
  2. The budget is not ours. The scheme imposes a timeout, typically 2 seconds end to end, of which our share is smaller still. Exceed it and the network answers on our behalf.
  3. Silence is the worst answer. A decline is a bad experience. A timeout means the scheme decides using rules we configured weeks ago, and we find out later.
functional, new
  1. Authorise or decline a card transaction within the network's budget.
  2. Place an auth hold at authorisation; post entries only at capture.
  3. Handle partial capture, over-capture within tolerance, and expiry.
  4. Process daily clearing and settlement files and reconcile them.
  5. Support chargebacks through a defined dispute lifecycle.
  6. Continue authorising when the core ledger is unreachable.
non-functional, new
  1. Authorisation p99 under 500 ms inside our boundary, against a 2-second scheme budget.
  2. Authorisation availability 99.99%, higher than any other path in the bank.
  3. No card data in our primary systems. PCI scope minimised by design.
  4. A capture must post exactly once even though clearing files are redelivered.
the tension to state early
Authorisation needs 99.99% availability and the ledger deliberately chose consistency over availability back in round one. Those two requirements point in opposite directions, and resolving that tension is what chapters 90 and 91 are for. Saying so up front shows you noticed rather than stumbled into it.
89

The four-party model

Knowing the participants and where the money and the fees flow is the vocabulary for everything after.

PartyRoleUs?
CardholderOur customer, holding the cardOur customer
IssuerIssued the card, holds the funds, makes the authorisation decisionThis is us
AcquirerThe merchant's bank, submits transactions into the schemeSomeone else
MerchantSells the goods, receives settlement from the acquirerSomeone else's customer
SchemeVerve, Mastercard, Visa. Routes messages, sets rules, performs settlementOur counterparty

Where the money goes, and where it stops

worked numbers
          customer pays ₦10,000 at a merchant


          merchant receives    ₦9,800   (after acquirer's merchant discount)

          acquirer keeps         ₦50

          scheme keeps          ₦30   (scheme fee)

we receive            ₦120  (interchange, paid TO the issuer)


interchange is revenue for us: the issuer is paid for taking

the credit and fraud risk on the transaction.


          so our ledger shows: customer −10,000, and later +120 income
        
the fact that reframes the product
Card issuing is a revenue line, not a cost line. Every card purchase pays us interchange. That is why banks push cards, why interchange caps are politically contested, and why the authorisation path deserves the highest availability target in the bank: a declined transaction is lost revenue as well as an unhappy customer.

The three message flows

FlowWhenTimingMoney moves?
AuthorisationAt the terminal, in real timeUnder 2 secondsNo. A hold is placed
ClearingBatch, usually overnightHours to days laterYes. Entries are posted
SettlementNet position with the schemeDailyYes, in aggregate between institutions

The separation of authorisation from clearing is the single most important structural fact about cards, and it maps exactly onto the holds model built in Part 5. That is not a coincidence; holds exist because of flows like this.

90

Authorisation: the 2-second budget

Budget the time explicitly, because every check competes for the same milliseconds and the ordering of the checks is a design decision.

network transit, both ways ~600 ms
our share of the budget 1,000 ms
safety margin we keep 400 ms

Inside our 1,000 ms, spent in this order:

1. parse and validate message 2 ms
2. token → account lookup (cache) 3 ms
3. card state: active, not blocked 3 ms
4. velocity limits (Redis) 4 ms
5. fraud score (hard timeout) 150 ms
6. available balance + place hold 250 ms
7. build and sign response 2 ms
total p99 ~414 ms
why the order is exactly this
  1. Cheapest and most decisive first. A blocked card is rejected in 8 ms without troubling the ledger or the fraud model.
  2. Fraud before the hold, so a fraudulent transaction never touches the money path.
  3. The hold last, because it is the only step that writes, and writes are the expensive, contended operation.
  4. Every step has its own timeout that fits the remaining budget, and the budget is passed down as a deadline rather than assumed.
code
// deadline propagation: each step gets what is actually left, not a
// fixed timeout that could collectively exceed the budget.
async function authorise(msg: AuthMessage): Promise<AuthResponse> {
  const deadline = Date.now() + 1000;
  const left = () => deadline - Date.now();

  const card = await cards.lookup(msg.token, { timeout: Math.min(50, left()) });
  if (!card || card.state !== 'active') return decline('57');

  const vel = await limits.check(card, msg.amount, { timeout: Math.min(50, left()) });
  if (!vel.ok) return decline('61');

  // fraud FAILS OPEN. an unavailable model must not decline good business;
  // the asynchronous path in Part 9 catches what the fast path misses.
  const risk = await fraud.score(msg, { timeout: Math.min(150, left()) })
    .catch(() => ({ action: 'allow', degraded: true }));
  if (risk.action === 'block') return decline('59');

  // the hold FAILS CLOSED via stand-in: see the next chapter.
  const hold = await ledger.placeHold(card.accountId, msg.amount, {
    timeout: Math.min(250, left())
  }).catch(() => standIn.decide(card, msg));

  return hold.approved ? approve(hold.id) : decline('51');
}
fail open or fail closed, per dependency
This is the judgement call interviewers are listening for. Fraud fails open, because declining every transaction when the model is down costs more than the fraud it would have caught, and Part 9's asynchronous path still sees everything. The balance check fails to stand-in rather than open, because approving without any balance knowledge is how you lend money by accident. Different dependencies get different failure postures, chosen deliberately.
91

Stand-in processing and the offline window

The resolution of the tension from chapter 88. The ledger is CP by design and authorisation needs four nines. So authorisation stops depending on the ledger for every decision.

how stand-in works
  1. A balance snapshot per card account is replicated continuously to the authorisation tier, seconds behind the ledger.
  2. When the ledger is unreachable, the authoriser decides from the snapshot plus a conservative cap.
  3. Every stand-in approval is durably recorded locally before the response is sent.
  4. On recovery, the records are replayed to the ledger as holds, in order.
  5. The worst-case exposure is bounded and calculated in advance, not discovered afterwards.

The stand-in rules, and their arithmetic

code
interface StandInPolicy {
  maxPerTransaction: bigint;        // e.g. ₦20,000
  maxPerCardDuringOutage: bigint;   // e.g. ₦50,000
  maxTransactionsPerCard: number;   // e.g. 5
  requireSnapshotAge: number;       // decline if snapshot older than 300 s
  // merchant categories we refuse to stand in for: unrecoverable
  // or high-risk. cash, crypto, gambling.
  blockedMcc: string[];
}
worked numbers
          worst case exposure during a 15-minute outage:


          active cards likely to transact in 15 min ≈ 40,000

          × max per card during outage            = ₦50,000

          = theoretical maximum                  ₦2bn


          realistic: only accounts with insufficient funds create loss,

          historically ~0.3% of stand-in approvals

          → expected exposure ≈ ₦6m, against the cost of

          declining every card transaction for 15 minutes


the tradeoff is quantified, and that is what makes it defensible
the sentence that lands this chapter
"Authorisation cannot inherit the ledger's availability, so it does not depend on it for every decision. It uses a replicated snapshot and a conservative cap, records every approval locally, and replays on recovery. I accept a bounded, quantified overdraw risk in exchange for staying up, and I would bring that number to the business rather than choose it myself, because it is a risk-appetite decision expressed as a configuration value."
stand-in
authorising when the ledger is unreachable
swipe the figure sideways, or tap expand for full screen
1/9
request arrives
A terminal sends an authorisation request. We have under two seconds to answer, and the clock is the network’s, not ours.
92

Auth holds, partial capture, and expiry

The holds machinery from Part 5, now driven by a network rather than by us. Three cases, all routine, all needing explicit handling.

The normal case: hold then capture

authorisation: ₦10,000 at a supermarket · no entries written
holds table+1000000kind card_auth, expires in 7 days
ledger effectnoneavailable balance drops; ledger balance unchanged
capture, next day, from the clearing file · journal kind: card.capture
wallet A−1000000now the money actually moves
card_settlement:mastercard+1000000owed to the scheme at settlement
Σ0hold marked captured, linked to this journal id

Partial and over capture

CaseExampleHandling
Partial captureHeld ₦10,000, one item out of stock, captured ₦7,500Post ₦7,500. Release the remaining ₦2,500 of hold immediately rather than waiting for expiry.
Multiple capturesHeld ₦10,000, shipped in two parcels: ₦6,000 then ₦4,000Two journals against one hold. The hold closes when fully consumed.
Over capture within toleranceHeld ₦10,000 at a fuel pump, captured ₦10,800Scheme rules permit this for specific MCCs, typically up to 15 to 20%. Post the captured amount, and note it may push the account negative.
Over capture beyond toleranceHeld ₦10,000, captured ₦25,000Post it anyway, and flag for dispute. Refusing a valid scheme-presented clearing record is not an option; recovery is via chargeback.
ExpiryHeld ₦10,000, never capturedSweeper releases at the scheme's expiry window: 7 days typical, up to 30 for hotels and car rental.
the detail that generates support tickets
Release the unused portion of a hold at capture, not at expiry. A customer whose ₦10,000 hold sits for seven days after a ₦7,500 purchase has ₦2,500 they cannot spend for no reason, and they will call. Every good card processor releases eagerly, and mentioning it shows product empathy alongside the architecture.
93

Clearing and settlement files

Authorisation is real-time messaging. Clearing is batch file processing, and it is the authoritative record of what the scheme believes happened.

the daily cycle
  1. The scheme publishes a clearing file: every transaction presented against our cards.
  2. We match each record to an existing authorisation by the network reference.
  3. Matched records are captured: entries posted, hold consumed.
  4. Unmatched records are the interesting ones, and each needs a defined path.
  5. A settlement file states the net amount we owe the scheme or it owes us.
  6. One settlement payment moves through a rail from Part 6, and reconciliation in Part 10 proves it.
Unmatched caseCauseHandling
Clearing with no authorisationOffline terminal, forced acceptance, or a stand-in approval the scheme made for usPost it. The scheme's record governs. May overdraw the account, which is a recovery problem rather than a posting one.
Authorisation with no clearingMerchant never completed the saleLet the hold expire. Normal and common.
Amount mismatchPartial, over-capture, or tip addedPost the cleared amount; reconcile the difference against the hold.
Duplicate clearing recordScheme file redelivered or reprocessedIdempotency on the network reference. This is the mechanism that makes file reprocessing safe.
code
-- the idempotency key that makes clearing-file reprocessing safe.
-- schemes DO redeliver files, and operators DO re-run failed batches.
INSERT INTO journal (id, kind, idempotency_key)
VALUES ($jid, 'card.capture',
        'clr:' || $schemeRef || ':' || $clearingDate);
-- a redelivered file hits the unique constraint and posts nothing.
-- same pattern as the accrual batch in Part 5: make the key carry
-- enough identity that a re-run is provably a no-op.
worked numbers
          settlement is net, not gross:


          purchases by our cardholders        −₦840m  we owe

          refunds to our cardholders           +₦31m

          interchange earned                    +₦10m  revenue

          scheme fees                           −₦2m

net owed to scheme                 ₦801m


one payment settles millions of transactions, which is why the

card_settlement account balance must match the file exactly.
94

Debit against a wallet vs credit against a line

The same authorisation path serves both card types. What differs is which account is checked and which is debited, and the answer reuses Part 5 entirely.

Debit cardCredit card
Funds sourceThe customer's own walletA credit line, which is the bank's money
Authorisation checkavailable >= amountcredit_limit − utilised >= amount
Account debited at captureCustomer wallet (a liability of the bank)Card receivable (an asset of the bank)
InterestNoneAccrues after a grace period, on revolved balances
Statement cycleNone; each transaction is finalMonthly, with a minimum payment due
Bank's riskFraud onlyFraud plus credit risk
Interchange earnedLowerHigher, reflecting the credit risk taken
credit card capture of ₦10,000 · journal kind: card.capture.credit
card_receivable:C-4471−1000000the customer now owes us this
card_settlement:mastercard+1000000we owe the scheme at settlement
Σ0note the customer's wallet is not touched at all
the structural observation worth making
A credit card is an overdraft facility with a card attached and a statement cycle. Chapter 58's overdraft_facilities table, with its limit and its allowed-kinds gate, already models the authorisation logic. The genuinely new parts are the statement cycle and the grace period, both of which are scheduled batch jobs of exactly the shape built in chapter 60. Recognising that a new product is an existing product plus a schedule is how you avoid building a second system.
95

Multi-currency cards and DCC

A Nigerian card used in London. Three currencies are in play and a decision about who converts, which is both a product and a fairness question.

worked numbers
transaction currency  GBP  what the merchant charges

settlement currency   USD  what the scheme settles in

billing currency      NGN  what we debit the customer in


          two conversions, two spreads, two chances to be unfair
three ways to handle the conversion
  1. Multi-currency wallet. If the customer holds GBP, debit GBP directly. No conversion, no spread. Best for the customer and the reason multi-currency wallets are attractive.
  2. Issuer conversion. The scheme converts GBP to USD at its rate, we convert USD to NGN at ours. Two spreads, ours disclosed on the statement.
  3. DCC, dynamic currency conversion. The merchant's terminal offers to charge in NGN directly. Usually the worst rate of the three, because the merchant and acquirer share that margin.
code
// selecting the funding account: prefer a matching currency wallet,
// which avoids conversion entirely. this is a real customer benefit and
// it costs us the FX spread we would otherwise have earned, so it is a
// deliberate product choice rather than an oversight.
function selectFunding(card: Card, txCcy: string): Account {
  const match = card.wallets.find(w => w.currency === txCcy && w.available > 0n);
  if (match) return match;                // no conversion: cheapest for them
  return card.wallets.find(w => w.currency === card.billingCurrency)!;
}
the disclosure point, which is increasingly regulatory
The customer must be able to see what rate was applied and what margin we added. Storing both market_rate and customer_rate on every conversion, as chapter 83 did, is what makes that statement line possible. A design decision in round seven pays off as a compliance capability in round eight, which is worth pointing out because it demonstrates the coherence of the whole design.
96

Chargebacks and the dispute ledger

A cardholder disputes a transaction. What follows is a multi-stage process with deadlines, evidence and money moving back and forth, and it must all be on the ledger.

worked numbers
1. dispute raised          customer says "not mine"

                  ↓ we may provisionally credit them

2. chargeback             we claim against the acquirer

                  ↓

3. representment         merchant provides evidence and pushes back

                  ↓

4. pre-arbitration       one more exchange

                  ↓

5. arbitration            the scheme decides, and charges a fee to the loser


          each stage has a hard deadline. missing one forfeits the case
          automatically,

          which makes the deadline timer a correctness feature, not a reminder.
        
provisional credit while the dispute is investigated · journal kind: dispute.provisional
dispute_suspense−1000000the bank funds the customer meanwhile
wallet A+1000000customer made whole during investigation
Σ0if we lose, the suspense clears to a loss account instead
code
CREATE TABLE disputes (
  id              UUID PRIMARY KEY,
  journal_id      UUID NOT NULL,     -- the disputed capture
  reason_code     TEXT NOT NULL,     -- scheme-defined, drives evidence needed
  stage           TEXT NOT NULL,     -- raised|chargeback|representment|arbitration
  amount          BIGINT NOT NULL,
  currency        CHAR(3) NOT NULL,
  -- the single most operationally important column in the table.
  -- a missed deadline is an automatic loss, so this drives alerting.
  stage_deadline  TIMESTAMPTZ NOT NULL,
  provisional_credit_journal UUID,
  outcome         TEXT,             -- won | lost | withdrawn
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX disputes_due ON disputes (stage_deadline)
  WHERE outcome IS NULL;   -- the queue that must never be starved
why a fintech's dispute handling is often its weakest system
Disputes are low volume, high stakes, deadline driven and unautomatable, which is the exact profile engineering teams deprioritise. The money lost to missed deadlines is invisible in a dashboard because it looks like normal chargeback loss. Instrumenting the deadline queue and alerting on it is a cheap, high-value thing to propose, and proposing it signals operational experience.
97

PCI DSS scope, tokenisation, and the HSM

Card data is radioactive. The correct architectural goal is not to protect it well but to never hold it, so that most of the compliance burden does not apply to most of the system.

DataMay we store it?Notes
PAN, the 16-digit card numberOnly encrypted, in PCI scopeAvoid entirely. Store a token instead
Cardholder name, expiryYes, with protectionIn scope when stored alongside a PAN
CVV / CVCNever, under any circumstancesNot even encrypted. Prohibited after authorisation
Full magnetic stripe or chip dataNeverProhibited after authorisation
PIN or PIN blockNever outside an HSMPIN operations happen inside hardware only
Token, plus last 4 and BINYes, freelySufficient for display, routing and support

The architecture that shrinks scope

worked numbers
in PCI scope (small, isolated, separately audited):

            the card vault, the HSM, the authorisation message gateway


out of scope (everything else, which is the point):

            the ledger, product services, notifications, analytics,

            support tooling, the mobile app backend


the ledger stores a token and never a PAN, so the most

critical system in the bank sits outside PCI scope entirely.
what the HSM does, and why it must be hardware
  1. PIN verification without the PIN ever existing in software memory.
  2. Cryptogram validation for chip transactions, proving the card is genuine.
  3. Key management under keys that are physically non-exportable.
  4. Tamper response: physical intrusion causes immediate key destruction.
  5. The guarantee is that a full compromise of our servers does not yield the keys, because the keys never existed outside the hardware boundary. That is a property software cannot provide at any price.
the answer to give on PCI
"I would minimise scope rather than maximise protection. Card data lives in an isolated vault with an HSM, and everything else, including the ledger, holds only a token plus BIN and last four. That keeps the bank's most critical system out of PCI scope, which reduces audit surface, lets the ledger team deploy without a PCI review, and means a breach of the main platform exposes no card data. The security win and the delivery-velocity win are the same decision."
98

Sketch v8: card authorisation on the money path

What changed, and the cost accepted

ChangeDriven byCost accepted
A separate authorisation tier99.99% target against a CP ledgerA second decision path to keep consistent with the first
Stand-in processingMust authorise when the ledger is unreachableA bounded, quantified overdraw exposure
Deadline propagationA 2-second budget we do not ownEvery dependency needs a timeout that fits the remainder
Fraud fails open, balance fails to stand-inDifferent costs of being wrongMust be chosen and documented per dependency
Clearing file pipelineCapture is batch, not real timeUnmatched-record handling, and idempotency on the scheme reference
Dispute subsystem with deadlinesChargeback rights and scheme rulesA deadline queue that must be alerted on, or money is silently lost
Isolated card vault and HSMPCI DSSSeparate infrastructure and audit, in exchange for keeping everything else out of scope
how to close round eight
"v8 adds the first path in the bank that cannot inherit the ledger's consistency-first posture, so I gave it its own availability strategy rather than weakening the ledger. The three things I would defend hardest: a hold at authorisation and entries only at capture, which is Part 5's model doing exactly what it was built for; stand-in with a quantified exposure rather than declining everything during an outage; and PCI scope minimised so the ledger never sees a card number. What comes next is that someone is now deliberately attacking all of this."
architecture v8
the authorisation tier, and everything behind it
swipe the figure sideways, or tap expand for full screen
1/9
new entry point
Cards add a new entry point to the bank, and the first design decision is where the compliance boundary sits.