Skip to content
EntryTarget Blog
Go back
USE-CASE-01 · EN

Digital wallets: why every user deserves their own subledger

Per-user subledgers, float, settlements and yield. The right ledger architecture turns each of these from a problem into a commodity.

A digital wallet looks simple on the outside — the user has a balance, it goes up, it goes down, they transfer. On the inside, it’s a high-cardinality accounting problem running in real time. Every user is an account. Every interaction is a transaction with at least two entries. And the operator has to answer three questions for the regulator at any moment: where is the money, who owns it, and how did it get here.

The problem

The temptation is to start with a users table that has a balance column. It works in a prototype, it breaks in production. The moment two operations mutate the balance in parallel, the history stops reconciling and you discover, at 3 a.m., that the platform’s aggregate balance doesn’t match the pool account at the partner bank.

The balance is not the source of truth. The sequence of entries is. The balance is a derived snapshot — and if you can’t rebuild it from the entries, you don’t have a ledger, you have a counter with bugs.

Chart of accounts

A good chart of accounts for a wallet has three layers: user accounts (one per wallet), operational accounts (pool, revenue, fees) and external counterparty accounts (instant-transfer provider, card partner). Users live in their own domain — often millions of accounts — but share the same tree of types.

AccountKindNormalExample
user:<id>:cashLiabilityCreditUser’s available balance
user:<id>:pendingLiabilityCreditTransactions being confirmed
ops:pool:pixAssetDebitPool balance at the central-bank rail
ops:revenue:feesRevenueCreditFees collected
ops:float:yieldRevenueCreditYield on the float
ext:provider:payinsCounterpartyDebitReceivables from the provider

One transaction, four perspectives

Consider an instant-transfer cash-in of R$ 100.00, with a R$ 2.00 fee retained by the platform. In the product’s view, “the user deposited a hundred reais.” In the ledger’s view, three entries must happen atomically:

INSTANT-TRANSFER CASH-IN · R$ 100.00 · FEE R$ 2.00
Account Debit Credit
ops:pool:pix 100.00
user:u_42:cash 98.00
ops:revenue:fees 2.00
Totals 100 100

Adding up: debits R$ 100.00, credits R$ 100.00. Net zero. The platform earned R$ 2.00 of revenue. The user is R$ 98.00 richer in available balance. The pool went up by R$ 100.00. All of it in a single atomic transaction, with a single transaction_id compliance can pull on demand.

Transaction API

POST /v1/transactions
{
"idempotency_key": "pix_in_01JCK7...",
"metadata": {
  "source": "pix.inbound",
  "end_to_end_id": "E18236120202604...",
  "user_id": "u_42"
},
"entries": [
  { "account": "ops:pool:pix",     "direction": "debit",  "amount": "100.00" },
  { "account": "user:u_42:cash",   "direction": "credit", "amount": "98.00"  },
  { "account": "ops:revenue:fees", "direction": "credit", "amount": "2.00"   }
]
}

Architecture in your AWS

EntryTarget’s runtime sits on a private subnet inside your VPC. Your application talks to it over internal gRPC — traffic never leaves your account. The transactional Postgres is yours, provisioned via IaC, encrypted with keys from your KMS. The payment provider (or partner bank) talks to your application, and only to your application. The ledger never exposes public endpoints.

Invariants that must not fail

InvariantWhyEnforced at
Σ debits = Σ credits per transactionDouble-entry; no way to relax itTransaction commit
User balance ≥ 0 (unless a credit line is attached)No accidental negative balancesPre-commit constraint
Σ user balances ≤ pool balanceReconciliation with the real partner-bank accountDaily reconciliation
Every transaction carries a unique idempotency_keySafe retries under flaky networksAPI gateway + DB
History is append-onlyAudit and complianceDB + KMS

Reconciliation

The biggest operational hole in fast-growing wallets is reconciliation between the internal ledger and partners’ statements — central bank, custodian bank, card networks. When every transaction has an external_id bound to a transaction_id, reconciliation becomes a JOIN. When it doesn’t, it becomes a spreadsheet.

The daily reconciliation runs a simple query: for every row on the external statement, there is exactly one matching ledger entry with the same end_to_end_id and the same amount. Mismatches land in an exception queue. Steady-state, a wallet with millions of users closes the day with zero or a few dozen exceptions — not hundreds of thousands.

Float and yield

When the aggregate user balance sits overnight in the pool, it earns. That revenue — the float — belongs to the platform, not to the user (unless you explicitly pay interest on the balance). A proper ledger books the yield daily into ops:float:yield without touching any user balance. No manual spreadsheet math at month-end.

If your aggregate balance doesn’t reconcile with the partner bank’s statement, you don’t have a wallet problem. You have a solvency problem.

— Universal rule

FAQ

Do I need double-entry even with a single currency?

Yes. The benefit isn’t multi-currency — it’s the mathematical guarantee that every transaction balances. Single-entry on a scalar balance fails silently; double-entry cannot fail silently.

Where do I store the user’s balance?

You don’t. You derive it — SELECT SUM(amount) over the user’s account, or an incrementally materialized view the ledger maintains. Balance is a projection; the entry is the source of truth.

Isn’t that slow for millions of users?

A modern ledger indexes by account + timestamp and materializes balances incrementally. Reads in milliseconds, writes in milliseconds. The complexity is operational — not performance.

How do I handle reversals and chargebacks?

With a new transaction of opposite direction that references the original via reverses_transaction_id. Never by editing old entries. The history stays append-only and auditable.

When you need this

  1. 1
    Your application holds balance on behalf of the user
    You are custodying someone else's money. Both the regulator and the user expect clean books.
  2. 2
    You reconcile with one or more external partners
    Each partner ships a file, a statement, a webhook. The ledger is the common point.
  3. 3
    You charge variable fees per operation
    Without a ledger, revenue leaks or gets double-counted.
  4. 4
    You have to close the month on time
    Without a ledger, closing is manual reconstruction. With a ledger, it's a report.
DEPLOY IN YOUR AWS

A ledger that’s actually yours.

entrytarget.com →


Previous Post
Marketplaces: split payments, escrow and payouts without a spreadsheet