The Maker-Checker Pattern for Fintech Ops
RBAC confirms the person has the right role, but it can't confirm the action is correct.
Your admin panel has a button that changes fee percentages for every institution on the platform. Another button overrides an OFAC sanctions screening. A third triggers a settlement batch that moves real money between real bank accounts.
Every one of those buttons works exactly as designed. The person clicking them has the right role. They passed authentication. RBAC says yes.
But RBAC answers a narrow question: does this person have permission to do this? It doesn't answer the question that actually matters in a financial system: should this specific action happen right now?
That gap between "permitted" and "correct" is where fintech incidents live. And the pattern that closes it has been around longer than most of the software we build on top of it.
What RBAC actually covers
A typical admin system has three roles. A viewer can inspect but not change. An operator handles routine management. An admin can modify system configuration.
This works. A viewer can't accidentally change a fee percentage. An operator can't reassign admin roles. The hierarchy prevents people from doing things outside their job.
But consider what happens inside a single role. An admin with full privileges sits down and does any of the following:
- Overrides a charity's OFAC screening status from "flagged" to "clear"
- Changes an institution's fee percentage from 3% to 4%
- Triggers a fee settlement batch outside the normal schedule
- Disables safeguard evaluation for a specific institution
Each action is legitimate. Each one is also a potential compliance violation, a financial error, or the kind of mistake that generates an incident report. RBAC can't distinguish between the admin who meant to change the fee from 3.0% to 3.1% and the one who fat-fingered it to 30%.
RBAC operates on identity and role, so it has no opinion about whether a specific action, at a specific moment, with specific parameters, is a good idea.
The second pair of eyes
Maker-checker splits every sensitive action into two steps. One person initiates the action (the maker). A different person reviews and approves it (the checker). The system doesn't execute until the second approval arrives.
Think of it like a construction site inspection. The electrician wires the panel, and a separate inspector signs off before the power gets turned on. Even if the electrician is the best in the city, the inspection still happens, because a wiring mistake can burn down the building.
In a fintech admin panel, the flow looks like this. An admin requests a fee change from 3% to 4% for Institution X. The system writes the request to a pending queue and notifies a second admin.
That second admin opens the request, sees the current value (3%), the proposed value (4%), and the institution it affects. They approve or reject.
Only on approval does the system execute the change.
For a two-person ops team, this might be a Slack notification and a button in the admin UI. For a larger organization, it's a formal workflow with SLAs, escalation paths, and audit trails.
The heuristic: which actions need it
Not every admin action needs a second approval. Adding a new customer to the platform is routine. Running a standard report is routine. The overhead of maker-checker on every click would grind operations to a halt.
So you need a filter. The heuristic I use has three criteria. If an action can cause any of these, it gets maker-checker:
Direct financial harm. Changing fee percentages, triggering settlement batches, modifying payout schedules, adjusting account balances. Anything where a wrong value moves real money in the wrong direction.
Compliance violation. Overriding sanctions screening, modifying KYC verification status, disabling transaction monitoring for an entity. These actions create regulatory exposure that can result in fines, consent orders, or worse.
Data integrity corruption. Manual ledger interventions, bulk data modifications, overriding system-generated calculations. Once ledger data is wrong, every downstream report, audit, and reconciliation inherits the error.
If an action doesn't hit any of these three, it's probably safe to execute on a single approval. If it hits two or three, consider requiring more than one checker.
What the approval queue looks like
The older pattern embeds approval logic into every sensitive table. A fee_changes table gets approved_by and approved_at columns. A screening_overrides table gets the same. Every table that holds a sensitive action grows its own mini-workflow.
This works until you have fifteen sensitive operations spread across eight tables. Now your approval logic is scattered, your audit trail is fragmented, and adding a new sensitive action means modifying a schema.
The better architecture is a centralized approval queue. One pending_actions table sits between the request and the execution.
CREATE TABLE pending_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
action_type TEXT NOT NULL,
payload JSONB NOT NULL,
requested_by UUID NOT NULL REFERENCES users(id),
requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
reviewed_by UUID REFERENCES users(id),
reviewed_at TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'pending',
CHECK (status IN ('pending', 'approved', 'rejected', 'expired')),
CHECK (requested_by != reviewed_by)
);The last constraint is the most important. The person who requests an action cannot be the person who approves it. Without this, a single admin can approve their own changes, and you've built a maker-checker system that doesn't actually check anything.
The payload column holds the serialized action: the target entity, the old value, the new value, any context the reviewer needs to make a decision. When the checker opens the request, they see the full picture, not just "fee change requested."
When an action is approved, a background worker picks it up, deserializes the payload, and executes it. The execution is logged separately, so you have a complete chain: who requested, who approved, when it executed, what changed.
The state machine
A pending action moves through four states: pending, approved, rejected, or expired. Expiration matters because a fee change requested on Monday might not make sense by Friday. Actions shouldn't sit in the queue indefinitely.
The transitions are:
- Pending to approved: A different user with the appropriate role reviews and approves. The system executes the action.
- Pending to rejected: A reviewer declines. The requester is notified with the reason.
- Pending to expired: The action sits unclaimed past a configurable TTL. 24 hours is a reasonable default for most operational actions, shorter for anything time-sensitive.
- Approved to executed: The background worker runs the action and records the result.
No other transitions are valid. An approved action can't be unapproved. A rejected action can't be resurrected. If the requester still wants the change, they submit a new request. This keeps the audit trail clean: every row in pending_actions represents one complete decision, not a series of back-and-forth edits.
When maker-checker fails anyway
In August 2020, Citibank accidentally wired $900 million to Revlon's lenders. The bank had a multi-reviewer approval process, and multiple people signed off on the transaction, but it still went wrong.
The failure was the quality of the checking, not the absence of maker-checker. The reviewers anchored to the first approver's judgment instead of independently verifying the transaction details. The system had the right structure but the wrong behavior: the second pair of eyes wasn't actually looking.
This is the failure mode you can't solve with software alone. A maker-checker system prevents a single person from executing a sensitive action, but it can't prevent two people from making the same mistake. But it does shift the probability: two people independently making the same error on the same action is considerably rarer than one person making it alone.
The practical mitigation is to make the review interface explicit. Don't just show "approve/reject." Show the current state, the proposed change, a diff of what will change, and force the reviewer to acknowledge specific details before the approve button activates. Make it harder to rubber-stamp.
The audit trail
In a SOC 2 audit, the examiner wants to know what prevents a single person from doing something harmful.
Maker-checker gives you a concrete answer. Every sensitive action has a request record, an approval record, and an execution record. The audit trail shows who requested the change, who reviewed it, what the change was, and when it took effect. If an action was rejected, that's recorded too, along with the reason.
The trail should be append-only. No updates, no deletes. If an action was approved and later found to be wrong, the correction is a new action that goes through the same approval flow. The original approval record stays in the log, because an auditor needs to see what happened, not a sanitized version of what should have happened.
For retention, SOX requires auditors to keep workpapers for seven years. Most fintech companies default to the same window for their own operational records regardless of whether they're SOX-bound, because the cost of storing a few million audit rows is negligible compared to the cost of not having them when a regulator asks.
Sizing the overhead
The pushback on maker-checker is always speed. If every fee change needs a second approval, operations slow down. For a two-person team where the other admin is in a different timezone, a 30-second action becomes an 8-hour action.
This is a real cost, but count your sensitive actions per week. They're rare and high-impact by definition. If you're executing hundreds of fee changes or sanctions overrides per week, the problem isn't maker-checker overhead. Your system shouldn't need that many manual interventions.
For the genuinely time-sensitive cases, build an escalation path. If the primary checker doesn't respond within 30 minutes, notify a backup. If the backup doesn't respond within an hour, page the on-call. The queue shouldn't be a dead end.
Where RBAC and maker-checker meet
These two patterns aren't alternatives. They're layers. RBAC controls who can even see the maker-checker queue. An operator role might be able to request actions but not approve them. An admin role can do both, but the self-approval constraint means they still can't approve their own requests.
The combination handles two different threat models. RBAC stops the outsider who lacks the right role from performing a privileged action, while maker-checker catches the insider who has the right role but performs the wrong action, whether by mistake or intent.
Your SOC 2 auditor will ask both how you prevent unauthorized changes and how you prevent authorized mistakes. RBAC answers the first, the approval queue answers the second. If you only have one answer to both questions, you have a gap.
Sources
- Al Jazeera — $900M Gaffe: Citigroup Can't Recoup Revlon Payout - Reporting on the August 2020 accidental wire transfer, the $893M amount at issue, and the February 2021 court ruling
- SEC Final Rule 33-8180 — Retention of Records Relevant to Audits and Reviews - The SEC rule implementing SOX Section 802's seven-year retention requirement for audit workpapers
- Wikipedia — Maker-Checker - General definition of the maker-checker (four-eyes) principle in banking and financial operations
Frequently Asked Questions
Built by Trio, a fintech-native engineering partner helping teams build the next generation of financial technology and infrastructure.
Subscribe to Ledger Drift for high-signal insights into how modern fintech is built, from systems to code to teams.