Skip to main content

Inter-Bank Reconciliation with a Private Blockchain: A Modern Alternative to Traditional Settlement

· 9 min read
Prasad Kumkar
Founder & CEO, ChainScore Labs

Inter-bank reconciliation is one of the most expensive, slow, and error-prone processes in finance. Banks maintain independent ledgers. At the end of each day (or each settlement cycle), they compare records to identify discrepancies. Differences must be investigated, resolved, and re-posted — a process that takes hours to days and costs the industry billions annually.

A permissioned blockchain shared between participating banks eliminates this reconciliation process entirely. Instead of each bank maintaining an independent ledger and reconciling after the fact, all banks share a single, cryptographic ledger that updates in real time.

The Reconciliation Problem

How It Works Today

Bank A's Ledger          Bank B's Ledger          Bank C's Ledger
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Txn 001: +100│ │ Txn 001: -100│ │ │
│ Txn 002: +250│ ≠? │ Txn 002: -200│ ≠? │ Txn 003: -150│
│ Txn 003: +150│ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└───────────┬───────────┴───────────┬───────────┘
│ │
┌────┴───────────────────────┴────┐
│ End-of-Day Reconciliation │
│ • Compare ledgers │
│ • Identify discrepancies │
│ • Investigate (hours) │
│ • Resolve and re-post │
│ • Cost: $3-8 per transaction │
└─────────────────────────────────┘

Three separate sources of truth lead to three different versions of reality. Reconciliation isn't verifying correctness — it's resolving contradictions between systems that should agree but don't.

Where Discrepancies Come From

  • Timing differences: Bank A records a transfer at 3:00 PM. Bank B records it at 3:04 PM. Their end-of-day snapshots don't match.
  • Processing failures: Bank A's system is down for 15 minutes. Transactions during that window are processed after reconciliation has already started.
  • Data entry errors: Account numbers mistyped, amounts transposed, currency codes mismatched.
  • Fee differences: Bank A charges a $5 wire fee. Bank B charges $3. Neither knows the other's fee structure.
  • Currency conversion: Bank A records in USD. Bank B records in EUR. Exchange rates fluctuate between transaction time and reconciliation time.

The Cost

According to industry estimates, manual reconciliation costs banks $3-8 per transaction in operational overhead. For a mid-size bank processing 50,000 inter-bank transactions daily, that's $150,000-$400,000 per day — or $55-146 million per year — spent on a process that shouldn't exist.


How a Shared Blockchain Changes the Model

Instead of each bank maintaining an independent ledger (and reconciling discrepancies later), all participating banks share one distributed ledger:

    ┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│ Bank A │ │ Bank B │ │ Bank C │
│ (Node 1) │ │ (Node 2) │ │ (Node 3) │
│ │ │ │ │ │
│ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │
│ │MiniLedger│◄┼──────┼─┤MiniLedger│◄┼──────┼─┤MiniLedger│ │
│ │ (Raft) │ │ │ │ (Raft) │ │ │ │ (Raft) │ │
│ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└──────────────────────┼──────────────────────┘

┌──────────┴──────────┐
│ Shared Ledger │
│ │
│ • Single source of │
│ truth for all │
│ inter-bank txns │
│ • Real-time updates │
│ • Cryptographic │
│ non-repudiation │
│ • SQL-queryable │
│ for instant net │
│ position calc │
└─────────────────────┘

Every inter-bank transaction is submitted to the shared ledger. All banks see the same transaction at the same time. There is nothing to reconcile because there is only one version of the truth.


Implementation

Step 1: Consortium Setup

Three banks — AlphaBank, BetaFinance, and GammaTrust — form a reconciliation consortium. Each runs a MiniLedger node:

# AlphaBank (bootstrap node)
npm install miniledger
npx miniledger init --data-dir /var/lib/alpha-ledger
npx miniledger start --bootstrap --p2p-port 4442 --http-port 4441

# BetaFinance joins
npx miniledger init --data-dir /var/lib/beta-ledger
npx miniledger join --bootstrap ws://alphabank-node:4442/ws

# GammaTrust joins
npx miniledger init --data-dir /var/lib/gamma-ledger
npx miniledger join --bootstrap ws://alphabank-node:4442/ws

Each bank has their own node running on their own infrastructure. No one bank controls the network.

Step 2: Account Registration

Each bank registers the accounts involved in inter-bank transfers:

// AlphaBank registers its nostro/vostro accounts
await ledgerNode.submit({
key: 'account:alphabank:nostro:betafinance',
value: {
bank: 'alphabank',
account_type: 'nostro',
counterparty: 'betafinance',
currency: 'USD',
account_number: 'AB-USD-BF-001',
balance: 5000000.00
}
});

await ledgerNode.submit({
key: 'account:betafinance:vostro:alphabank',
value: {
bank: 'betafinance',
account_type: 'vostro',
counterparty: 'alphabank',
currency: 'USD',
account_number: 'BF-USD-AB-V01',
balance: 5000000.00
}
});

Step 3: Real-Time Transaction Submission

When AlphaBank processes a customer wire transfer to a BetaFinance account, it submits the transaction to the shared ledger:

async function submitInterBankTransfer(senderBank, receiverBank, details) {
const txnKey = `txn:${senderBank}:${Date.now()}:${crypto.randomUUID().slice(0, 8)}`;

await ledgerNode.submit({
key: txnKey,
value: {
type: 'inter_bank_transfer',
status: 'submitted',
sender: {
bank: senderBank,
account: details.senderAccount,
customer_ref: details.customerReference
},
receiver: {
bank: receiverBank,
account: details.receiverAccount,
customer_name: details.customerName
},
amount: details.amount,
currency: details.currency,
fees: details.fees || 0,
purpose_code: details.purposeCode,
submitted_at: new Date().toISOString(),
submitted_by: senderBank
},
privacy: {
readers: [
`pk_node-${senderBank}`,
`pk_node-${receiverBank}`,
'pk_node-regulator'
],
writers: [`pk_node-${senderBank}`],
public: false
}
});

return txnKey;
}

// Usage
const txnId = await submitInterBankTransfer('alphabank', 'betafinance', {
senderAccount: 'AB-CUST-4421',
receiverAccount: 'BF-CUST-8802',
customerReference: 'INVOICE-2026-8842',
customerName: 'Acme Corp',
amount: 150000.00,
currency: 'USD',
fees: 25.00,
purposeCode: 'TRADE_SETTLEMENT'
});

Step 4: Transaction Acknowledgment

BetaFinance receives the transaction on the shared ledger and acknowledges it:

async function acknowledgeTransfer(txnKey, receiverBank) {
const txn = await ledgerNode.query(`
SELECT value FROM world_state WHERE key = ?
`, [txnKey]);

if (!txn.length) throw new Error('Transaction not found');

const details = JSON.parse(txn[0].value);

// Verify the receiver is this bank
if (details.receiver.bank !== receiverBank) {
throw new Error('Transaction not addressed to this bank');
}

// Credit the receiving customer's account in the bank's internal system
await internalBankSystem.creditAccount(
details.receiver.account,
details.amount,
`Inter-bank transfer from ${details.sender.bank}, ref: ${details.sender.customer_ref}`
);

// Acknowledge on the shared ledger
await ledgerNode.submit({
key: txnKey,
value: {
...details,
status: 'acknowledged',
acknowledged_at: new Date().toISOString(),
acknowledged_by: receiverBank,
receiver_credit_confirmed: true
},
privacy: {
readers: [
`pk_node-${details.sender.bank}`,
`pk_node-${receiverBank}`,
'pk_node-regulator'
],
writers: [`pk_node-${receiverBank}`],
public: false
}
});
}

Step 5: Net Position Calculation

At any moment, any bank can calculate their net position with all counterparties:

async function calculateNetPosition(bankId) {
// Outgoing transfers (this bank sent)
const sent = await ledgerNode.query(`
SELECT
json_extract(value, '$.receiver.bank') as counterparty,
json_extract(value, '$.currency') as currency,
SUM(CAST(json_extract(value, '$.amount') AS REAL)) as total_sent,
COUNT(*) as txn_count
FROM world_state
WHERE key LIKE 'txn:%'
AND json_extract(value, '$.sender.bank') = ?
AND json_extract(value, '$.status') IN ('submitted', 'acknowledged', 'settled')
GROUP BY counterparty, currency
`, [bankId]);

// Incoming transfers (this bank received)
const received = await ledgerNode.query(`
SELECT
json_extract(value, '$.sender.bank') as counterparty,
json_extract(value, '$.currency') as currency,
SUM(CAST(json_extract(value, '$.amount') AS REAL)) as total_received,
COUNT(*) as txn_count
FROM world_state
WHERE key LIKE 'txn:%'
AND json_extract(value, '$.receiver.bank') = ?
AND json_extract(value, '$.status') IN ('submitted', 'acknowledged', 'settled')
GROUP BY counterparty, currency
`, [bankId]);

return { sent, received, net_position: computeNet(sent, received) };
}

// Example output:
// AlphaBank Net Position:
// vs BetaFinance: +$2,150,000 (received more than sent)
// vs GammaTrust: -$850,000 (sent more than received)
// Net: +$1,300,000

Step 6: Settlement Batch Processing

At the end of each settlement cycle, automate the batch settlement using smart contracts:

const settlementContract = {
executeSettlement(ctx, cycleId) {
// Get all unsettled transactions for this cycle
const pendingTxns = ctx.query(`
SELECT key, value FROM world_state
WHERE key LIKE 'txn:%'
AND json_extract(value, '$.status') = 'acknowledged'
`);

// Group by bank pair
const obligations = {};
for (const txn of pendingTxns) {
const { sender, receiver, amount } = JSON.parse(txn.value);
const pairKey = `${sender.bank}|${receiver.bank}`;
obligations[pairKey] = (obligations[pairKey] || 0) + amount;
}

// Calculate net settlement amounts (bilateral netting)
const netSettlements = bilateralNet(obligations);

// Record settlement instructions
for (const settlement of netSettlements) {
ctx.set(`settlement:${cycleId}:${settlement.from}_${settlement.to}`, {
type: 'settlement_instruction',
cycle_id: cycleId,
from: settlement.from,
to: settlement.to,
amount: settlement.amount,
currency: 'USD',
status: 'pending_rtgs',
created_at: new Date().toISOString()
});

// Mark transactions as settled
ctx.log(`Settlement: ${settlement.from}${settlement.to}: $${settlement.amount}`);
}

return netSettlements;
}
};

Step 7: Regulatory Reporting Dashboard

Regulators get a read-only node with full SQL access to all transactions:

-- Daily inter-bank volume by currency pair
SELECT
json_extract(value, '$.currency') as currency,
DATE(json_extract(value, '$.submitted_at')) as date,
COUNT(*) as transaction_count,
SUM(CAST(json_extract(value, '$.amount') AS REAL)) as total_volume,
SUM(CAST(json_extract(value, '$.fees') AS REAL)) as total_fees
FROM world_state
WHERE key LIKE 'txn:%'
AND json_extract(value, '$.submitted_at') > datetime('now', '-30 days')
GROUP BY currency, date
ORDER BY date DESC, total_volume DESC;
-- Suspicious transaction patterns (round-dollar amounts > $10K)
SELECT
key,
json_extract(value, '$.sender.bank') as sender,
json_extract(value, '$.receiver.bank') as receiver,
json_extract(value, '$.amount') as amount,
json_extract(value, '$.submitted_at') as timestamp
FROM world_state
WHERE key LIKE 'txn:%'
AND CAST(json_extract(value, '$.amount') AS REAL) >= 10000
AND CAST(json_extract(value, '$.amount') AS REAL) = ROUND(CAST(json_extract(value, '$.amount') AS REAL))
AND json_extract(value, '$.submitted_at') > datetime('now', '-7 days')
ORDER BY amount DESC;
-- Settlement efficiency: submitted vs acknowledged time gap
SELECT
AVG(
(julianday(json_extract(value, '$.acknowledged_at')) -
julianday(json_extract(value, '$.submitted_at'))) * 86400
) as avg_acknowledgment_seconds,
MAX(
(julianday(json_extract(value, '$.acknowledged_at')) -
julianday(json_extract(value, '$.submitted_at'))) * 86400
) as max_acknowledgment_seconds
FROM world_state
WHERE key LIKE 'txn:%'
AND json_extract(value, '$.status') = 'acknowledged'
AND json_extract(value, '$.acknowledged_at') IS NOT NULL;

Privacy and Competitive Considerations

Banks are competitors. They do not want competitors to see their transaction volumes, customer relationships, or fee structures. The per-record encryption model handles this:

  • Each inter-bank transaction is visible only to the sender, receiver, and regulator.
  • Third banks see the transaction exists (for auditability and double-spend prevention) but the amount, counterparties, and customer details are encrypted ciphertext.
  • Regulator nodes have a decryption key for compliance monitoring but operate under strict access controls with immutable audit trails of their own queries.
// Only the sender, receiver, and regulator can read this transaction
privacy: {
readers: ['pk_node-alphabank', 'pk_node-betafinance', 'pk_node-regulator'],
writers: ['pk_node-alphabank'],
public: false // GammaTrust cannot read the details
}

Key Takeaways

  1. Eliminate reconciliation, don't automate it. A shared blockchain removes the need for reconciliation entirely. There's one ledger, one truth.

  2. Real-time position monitoring. Every bank can see their net position at any moment with a SQL query — no end-of-day batch process.

  3. Regulatory transparency built in. Regulators get a read-only node with full SQL access. No data requests, no manual reporting, no delays.

  4. Competitive data stays private. Per-record encryption ensures each transaction is visible only to the sender, receiver, and regulator.

  5. Settlement can be automated. Smart contracts handle netting and settlement instruction generation, reducing the operational overhead of manual settlement cycles.


Read the SQL queries guide for financial reporting patterns, or the privacy and encryption guide for competitive data protection.


About the Author

Prasad Kumkar is the Founder & CEO of ChainScore Labs. Over the last 5+ years, he has worked with teams building exchanges, DeFi infrastructure, smart contracts, tokenization systems, and protocol-level blockchain products, helping founders make architecture, security, and go-live decisions for production Web3 systems.