← All projects

Wallet, ledger and payments

The money system travel agencies use to pay for bookings: card top-ups through Stripe, wallets in their own currency, a ledger where every amount balances, and checks that catch any difference from Stripe's records.

40

currencies configured for agency wallets

Six of them, such as SAR and AED, pegged to the US dollar

7

kinds of Stripe event handled

Each one processed once, even if Stripe sends it twice

2

people needed to approve any money correction

One proposes, a second approves

The problem

Travel agencies on the platform pay for bookings from a prepaid wallet. They top it up by card or bank transfer, in their own currency. Every booking, refund and correction moves money, and one wrong number damages trust.

Payments also fail in untidy ways: the same payment notice can arrive twice, two bookings can try to spend the same balance at the same moment, and records on the payment provider's side can drift from ours.

What I did

  • Built the Stripe integration: card top-ups through Stripe Checkout, correct handling of currencies with 0, 2 or 3 decimal places, and a fee calculation so the wallet receives exactly the amount the agency asked for.
  • Made Stripe notifications safe to receive twice: each event is stored once by its unique ID, checked before it is processed again, and can be retried from an admin screen if processing fails.
  • Built a double-entry ledger: every money movement is recorded as a balanced pair, written in one database transaction.
  • Built two-phase purchase locking: money is first held, then charged or released, so two purchases can never spend the same balance.
  • Built reconciliation: a scheduled job compares top-ups with Stripe's records currency by currency and opens a case for any difference. A second person must approve every correction, and repeated rejections escalate to a senior.
  • Handled refunds and card disputes: money under dispute is locked and a case is opened.
  • Built the finance screens in the Admin Hub: top-ups, ledger, reconciliation cases, currency settings with change history, and refunds.
  • Moved webhook processing and reconciliation onto Google Cloud Tasks, so slow work runs in the background.

How money moves

Each step is in plain words. Open “Technical detail” for the engineering version.

  1. Top up

    An agency pays by card on a Stripe page, in its own currency.

    Technical detail

    Stripe Checkout and payment intents. Amounts are kept in each currency's smallest unit with the right number of decimals. Pegged currencies such as SAR and AED use their official rate; others use a market rate plus a margin.

  2. Receive the payment notice

    Stripe tells us the payment succeeded. The notice is recorded first and processed second, and the same notice is never processed twice.

    Technical detail

    A unique key on provider and event ID, a status check before re-queuing, a processing lock that expires after 30 minutes, and an admin retry. Processing runs asynchronously on Cloud Tasks.

  3. Credit the wallet

    The ledger records the money leaving one account and arriving in the agency's wallet, as one balanced pair.

    Technical detail

    An append-only journal where each entry is a balanced debit and credit written in one transaction, with a foreign-exchange clearing account for cross-currency movement.

  4. Spend

    When an agent books, the price is first held in the wallet. If the booking succeeds it is charged; if it fails, the hold is released.

    Technical detail

    Phase one raises the locked balance under an optimistic version check. Phase two writes the ledger entries. A sweep releases holds that were never finished.

  5. Check

    A scheduled job compares our records with Stripe's for the last two days and opens a case for anything that does not match.

    Technical detail

    A per-currency comparison over the previous two days. Case types include a record missing on either side, an amount mismatch and a status mismatch. Resolution is maker-checker, with escalation after repeated rejections.

Results

  • 40 currencies configured for agency wallets, six of them pegged to the US dollar.
  • A repeated Stripe notice cannot credit a wallet twice.
  • Every correction to an agency's money needs two people.
  • A disputed card payment locks the money and opens a case.

Timeline

  1. Feb 2026Stripe webhooks, card top-ups and the first wallet and ledger screens.
  2. Mar 2026Reconciliation with two-person resolution and escalation, background processing on Cloud Tasks, and bank-transfer top-ups.
  3. Apr 2026US dollar as the base currency, with each agency's local currency shown alongside.
  4. Sep 2026Disputes and Stripe refunds open a case; wallets held in each agency's own currency.

What I learned

In payments, receiving the same message twice is normal, so every step has to be safe to repeat.

Every locking choice is a trade-off. An optimistic lock never makes anyone wait, but when many requests hit one wallet at once, most are turned away and have to retry.

Words on this page

Wallet
A prepaid balance an agency uses to pay for bookings.
Ledger
The permanent record of every money movement.
Double-entry
Recording every movement twice, as money out of one account and into another, so the totals always balance.
Webhook
A message Stripe sends when something happens, such as a successful payment.
Idempotent
Safe to do twice: the second time changes nothing.
Reconciliation
Comparing our records with the payment provider's to find any difference.
Maker-checker
One person proposes a change and a second person approves it.

Built with

NestJSPostgreSQLPrismaStripeGoogle Cloud TasksNext.js