Connecting My Homemade App to My Homemade Bank Until Real Money Actually Moves — Adding Account Transfer to Payment, Confirmed End-to-End

金融 決済 マイクロサービス E2E Security

Introduction

I’m building two systems separately, on my own. One is an all-in-one platform bundling multiple services (video and music rental, sales, and so on). The other is a bank-style system with real accounts. I introduced each in The Vision of Evolving a DVD Rental System into a DMM-Style Platform and What’s Actually Running in My Homemade Bank System.

I connected the two with a single line for the first time. Adding my homemade bank’s “account transfer” as a payment method on the platform side. This lets a user settle a rental payment with a transfer from their own bank account. And this didn’t stop at connecting them on paper. I ran it through a mock-free, real end-to-end pass, all the way until the balance actually moved. This article is a record of how I designed that integration and how I confirmed it end-to-end.

The transfer lands in an account at this homemade bank (Mainichi Bank / EVERY DAYS BANK).

The homemade bank's account screen the transfer lands in (data is stubbed, a demo of a personal project — not a real account)


The overall flow

When a user picks “account transfer” on the checkout screen, the platform requests a transfer from the bank behind the scenes, and once it completes, records the payment and confirms the product (here, a home-delivery rental).

User: picks "account transfer" on the payment screen and pays


Platform (= from the bank's perspective, a "merchant")
   ├─▶ ① Authenticates to the bank as a service (with merchant credentials) → gets a short-lived token
   ├─▶ ② Requests a transfer with that token (with an idempotency key)
   │          │
   │          ▼
   │       Bank (account, transfer) ──▶ balance moves

   └─▶ ③ Transfer complete → records the payment, confirms delivery

Simple at a glance, but there are several design decisions here that must not be dropped.


Service-to-service auth: with merchant credentials, not the user’s

First, when the platform asks the bank for a transfer, under whose authority does it ask?

You must not reuse the user’s login credentials. To the bank, the platform is not the user — it’s a merchant. So the platform authenticates to the bank as a service using its own credentials (client credentials) and receives a short-lived, one-time token. The token has a purpose baked in — “this is for merchant payment” — and the destination customer is already resolved. All of the bank’s secrets stay closed inside the bank; the platform side only ever receives and uses a token.

In code, the platform side does “map → fetch token → transfer” using only its merchant credentials.

// Platform side: authenticate to the bank as a merchant → short-lived token → transfer
UUID   bankCustomerId = mapping.resolve(everydaysCustomerId); // map the member ID to the bank-side ID
String token   = bank.issueServiceToken(bankCustomerId);      // merchant credential, short-lived, purpose=payment
String idemKey = idempotency.newKey();                        // idempotency key to prevent double transfers
bank.transfer(token, bankCustomerId, amount, idemKey);        // Authorization: Bearer <token>

This way, the user’s credentials never fly back and forth on every payment, and the privileges are narrowed to “the minimum needed for payment.”


Even if a secret leaks, funds can’t be drained: destination allowlisting

A service-to-service token can leak someday. Design the damage on the assumption that it will.

If a merchant’s payment token leaked and an attacker could request a transfer to any arbitrary account, fund extraction would succeed. So I put a gate on the bank’s side. “A merchant’s payment token can only transfer to a registered merchant account.” Any other destination is refused outright, no questions asked (fail-closed).

Request a transfer with a merchant's payment token


Bank: is the destination a "registered merchant account"?
   ├─ Yes ─▶ execute the transfer
   └─ No ─▶ refuse (fail-closed)
        (= even if the merchant's secret leaks, not a single yen can move to someone else's account)
// Bank side: a merchant's payment token can only transfer to a "registered merchant account"
if (token.purpose() == MERCHANT_PAYMENT
        && !merchantAllowlist.contains(destinationAccountId)) {
    throw new DestinationNotAllowedException();   // → 403 (fail-closed)
}

Confine the damage of a leak to that one point — “a registered merchant account.” That way, even if the secret leaks, fund extraction cannot succeed.

And one more thing: an idempotency key is issued for every request, so the same transfer is never executed twice. This protects against a duplicate transfer caused by a network retry or a user’s double-click.


A mock doesn’t prove “it works”

This is the main topic.

While implementing the integration, I stood in a mock (WireMock) for the counterpart bank service and tested against it. A mock returns a fixed response to a fixed request. The tests went green, and it looked like “it works.”

But the moment I pulled the mock out and ran it all the way through against the actual services (a real end-to-end run), two blockers showed up. Both were mismatches that had been hidden precisely because the mock returned a predetermined response. I knocked them down one by one and, at the end, confirmed the real account’s balance actually moving.

【Mock (WireMock)】
   Payment ─▶ the fake bank returns "OK" ─▶ tests green … but a real mismatch stays hidden

【Real end-to-end run】
   Payment ─▶ run through the actual services ─▶ 2 hidden mismatches surface ─▶ fix them ─▶ balance moves

A mock shows that the code you wrote “behaves as you intended.” But a mismatch at the boundary with the other side stays hidden behind that predetermined response. Whether the integration truly holds together is only known once you run it through for real and see it all the way to the side effect (the balance moving).


You can only call it “connected” once the balance moves

Integration can only be proven with a real end-to-end run. Even if mock tests are green, that’s only your own side. A mismatch at the boundary with the other party is exactly what a mock hides. In the end, run it through the real thing and confirm the intended side effect (for money, the balance change) actually happens.

Service-to-service should use minimum-privilege merchant credentials, not the user’s. On top of that, restrict the destination to registered ones (fail-closed), confining the damage to one point even if a secret leaks. Prevent double movement of money with an idempotency key.

Knock down “it’s meaningless if it doesn’t work later” up front. There’s never a good reason to put off verification. Once you’ve connected something, run the real thing through it right then.

Two homemade systems were connected by a line of money for the first time. They were already connected on the design diagram, but only once the balance actually moved could I really call it “connected.”


Feel free to send a message

Job offers, project referrals, feedback, questions — anything is welcome. I sincerely hope to connect with people who share high ambitions. I will keep taking on the challenges I have staked my life on. Thank you very much.