> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The Wallet

> One global balance per person, in kobo — topped up in advance so a renewal never fails for lack of funds. Atomic, idempotent, overspend-proof.

The wallet is the money side of the [universal account](/identity/universal-identity). Every person has **exactly one wallet**, keyed to their account, holding a single balance in integer **kobo** — the same person, the same balance, at every merchant they subscribe to. It exists for one reason: a customer can put money in *ahead of time* so a renewal draws from a funded balance instead of racing a card that might decline.

## One balance, keyed to the account

```mermaid theme={null}
flowchart TB
    EMAIL["email (verified)"] --> ACC["CustomerAccount<br/>(core schema)"]
    ACC --> W["Wallet<br/>balance · currency · status"]
    W --> T1["WalletTransaction<br/>credit · topup"]
    W --> T2["WalletTransaction<br/>credit · virtual_account_funding"]
    W --> T3["WalletTransaction<br/>debit · subscription_charge"]
    W -.funds renewals at.-> A["Merchant A"]
    W -.funds renewals at.-> B["Merchant B"]
```

The `Wallet` hangs off `CustomerAccount`, not off any tenant — like the account itself, it is deliberately **mode-agnostic and merchant-agnostic**. Every movement writes a `WalletTransaction` row carrying the `type` (credit/debit), a `reason`, the `amount`, and the running `balanceAfter`, so the balance is always reconstructable from an append-only history.

## Credit and debit are atomic and idempotent

Money movement is the one place you cannot afford a double-apply or a race. Two properties make it safe:

* **Idempotent by `(account, reference)`.** Every mutation carries a caller-supplied `reference`, and `WalletTransaction` is `@@unique([accountId, reference])`. A webhook that fires twice, a retried job, a replayed request — the second insert is rejected by the database, and the caller gets back the *same* transaction with `alreadyApplied: true`. Value is granted once, ever.
* **Overspend-proof debits.** A debit is a single conditional `UPDATE … WHERE balance >= amount`. If the guarded update touches zero rows the balance was insufficient and the debit returns `{ ok: false, reason: 'insufficient_balance' }` — the balance can never go negative, even under concurrent debits.

```mermaid theme={null}
flowchart TD
    DEB["debit(amount, reference)"] --> DUP{"reference<br/>already applied?"}
    DUP -->|"yes"| SAME["return same txn<br/>alreadyApplied"]
    DUP -->|"no"| COND["UPDATE wallet<br/>SET balance -= amount<br/>WHERE balance >= amount"]
    COND --> HIT{"rows changed?"}
    HIT -->|"0"| INSUF["insufficient_balance<br/>(nothing moved)"]
    HIT -->|"1"| WRITE["write WalletTransaction<br/>with balanceAfter"]
```

<Note>
  A `P2002` unique-violation on the `reference` isn't an error — it's the idempotency guard doing its job. The service catches it and recovers the already-written transaction, so concurrent callers converge on one result.
</Note>

## Filling the wallet — two ways in

Money reaches the balance through exactly two credit paths, each with its own `reason` and its own idempotency reference:

<CardGroup cols={2}>
  <Card title="Top-up" icon="plus">
    A `topup` checkout charges an arbitrary ₦ amount on Nomba; the webhook credits the wallet with `reason: topup`, keyed to the Nomba transaction id. [See top-up →](/payments/top-up)
  </Card>

  <Card title="Virtual account" icon="building-columns">
    A bank transfer into the customer's dedicated [virtual account](/payments/virtual-account) fires a webhook that credits the wallet with `reason: virtual_account_funding`, keyed to the Nomba transaction id.
  </Card>
</CardGroup>

Both land in the same balance. A debit happens in one place — a **wallet-first renewal** — with `reason: subscription_charge`, referenced by the invoice. That is the whole point of the balance: [wallet-first billing](/billing/wallet-first-billing).

## In the ledger

Wallet movement is also double-entered into the finance [ledger](/architecture/data-model): a top-up posts a `wallet_topup` entry, and a wallet-funded renewal posts a `wallet_debit`. The wallet is its own `LedgerAccount`, so the money held on behalf of customers is a first-class balance in Duro's books, not an off-ledger side-effect.

Next: [wallet-first billing](/billing/wallet-first-billing) — how a renewal spends this balance before it ever touches a card.
