Digital Wallet

Ledgers tutorial for digital wallets

Overview

Technology companies with products that move money often need to hold balances for users or external counterparties. For example, your app might allow users to deposit funds into a wallet from which they can invest, or it might let your vendors accumulate points in a rewards wallet. This is a common use case across fintech, marketplaces, vertical SaaS, and e-commerce. Any time balances are being held, a unified data store is required to keep track of transactions and balances.

Ledgers is such a data store - an immutable financial database that allows companies to hold balances at scale.

This tutorial will explain how to use the Ledgers API for a digital wallet. In this example, we will assume you are building a service called SendCash that allows users to deposit money and send it to their friends. We will demonstrate a simple flow here with 3 payments:

  • A user deposits money in their wallet
  • The user sends money to a friend's wallet
  • The friend withdraws money

Additionally, there are some questions SendCash might ask, such as:

  • How much money does this user have in their SendCash account?
  • How much cash is in my bank account that holds all my user funds?

In order to facilitate these payment flows and be able to answer these questions, we will set up a ledger that has 3 ledger accounts.

  • Cash in SendCash's bank account
  • Funds held for a given user, Jane Doe
  • Funds held for another given user, John Doe

Who is this guide for

We assume you are a company interested in spinning up digital wallets as part of your product offering. This guide explains how to use Ledgers as the underlying infrastructure to power such digital wallets, and it is divided in three parts.

  • First, we will create a sample chart of accounts. Accounts are the balances your platform will track (more detail in our guide to ledger objects).
  • Next, we will work through a sample set of transactions. As business events happen in your digital wallet platform, you can use the guidance here to define how to write these transactions in the ledger. Transactions are logged in the ledger with the create ledger transaction endpoint. For sample API calls, review our quickstart guide for digital wallets.
  • Finally, we’ll discuss the use of metadata, transaction timing, account categories, and other features that can be helpful to any company launching a digital wallet.

Step 1. Designing Your Chart of Accounts

Ledgers implements double-entry accounting concepts. This ensures scalable consistency. If you are not familiar with debits and credits, start with our guide to debits and credits. We also recommend reviewing our guide to Ledgers Objects, as it explains ledger accounts, transactions, and categories in detail.

Balances are tracked on Ledgers by way of ledger accounts. Implementing a digital wallet requires the following set of accounts:

  • User balance accounts, which track balances exposed to users;
  • Cash accounts, which track cash positions associated with your digital wallet product;
  • Expense accounts such as fees, which track expenses incurred in your digital wallet product;
  • Revenue accounts which track different revenue streams captured by your digital wallet product;

User Balance Accounts

User balance accounts are credit normal accounts that track user balances. These are credit normal accounts because they represent funds your platform owes - or sources of funds. (For more, read our guide to debits and credits).

Your platform needs one ledger account per user. New accounts can be created using the create ledger account endpoint. Accounts can be enriched with free-form metadata. Here is a sample user balance account:

Account NameNormalityRepresentsIncreased By (Credits)Decreased By (Debits)Sample Metadata
User #1241241 BalanceCredit NormalBalance for user Robert Dylan.User deposits or credits.User withdrawals, fees.`accountType: "User Balance",

userFirstName: "Bob",

userLastName: "Dylan",

userState: "NY",

userActive: yes`

Cash Accounts

Cash accounts track different cash positions associated with the digital wallet app. They are debit normal given they represent funds your platform owns.

There are many different cash positions you might want to track in your digital wallet app:

  • Reserve funds that represent cash that needs to be available to users for withdrawals;
  • Operating cash accounts that represent pools of funds you direct income to and deduct expenses from;
  • Any other cash pools tied to operational or regulatory requirements.

Here is a sample cash account:

Account NameNormalityRepresentsIncreased By (Debits)Decreased By (Credits)Sample Metadata
SendCash Operating Cash AccountDebit NormalOverall cash position of SendCash appCash inflows of any kindCash outflows of any kindaccountType: "Cash Omnibus", active: yes

Expense Accounts

Expense accounts track expenses incurred in the regular operation of the digital wallet app. These can be debit normal when they represent expenses paid (as they reflect money outflows in a regular course of business) and credit normal when they represent expenses due (as they reflect payables).

A few types of expense accounts include:

  • Card fees incurred in the event of an account deposit or withdrawal;
  • Any taxes or fees to third parties paid on balances or transfers in the platform;
  • Banking fees associated with specific transactions.

Here is a sample expense account:

Account NameNormalityRepresentsIncreased By (Debits)Decreased By (Credits)Sample Metadata
Credit card fees paidDebit NormalTotal paid in credit card feesNew credit card fees paidTypically not decreased unless in the event of an adjusting entryaccountType: expense
Payable tor vendor XCredit NormalBalance due to vendor XNew amounts are recorded as due to vendor XPayoutsaccountType: expense,
vendor: X

We recommend holding payable balances only if necessary and using debit normal expense accounts to tally up expenses incurred whenever possible.

Revenue Accounts

Revenue accounts are counter to expense accounts and represent money inflows categorized as revenue by your digital wallet app. They are always credit normal accounts.

Different revenue accounts can be created to differentiate between revenue streams. Here are a few examples:

Account NameNormalityRepresentsIncreased By (Credits)Decreased By (Debits)Sample Metadata
Revenue from deposit feesCredit NormalRevenue incurred when users deposit funds in the appDepositsTypically ledger adjustmentsaccountType: revenue,
revenueStream: deposit_fees
Revenue from transfer feesCredit NormalRevenue incurred when users transfer funds from each other in the platformUser to user transfersTypically ledger adjustmentsaccountType: revenue,
revenueStream: transfer_fees

Step 2. Defining Transaction Logic

With a set of accounts mapped out, the next step is define transaction logic for your digital wallet app. The table below shows the most common transactions happening in a digital wallet app and how they get recorded on Ledgers.

We will walk through the following flows:

  • Deposit: a user adds money to their digital wallet account;
  • In-app transfer: two users send money to each other;
  • Withdrawal: user takes money out of their balance. In this example we will assume your app charges a 2% fee on withdrawals, which is recognized as revenue;

For a refresher on account normality and how debits and credits work review our guide to debits and credits.

Sample TransactionDebited AccountsCredited AccountsNotes
DepositCash account (increase)User balance account (increase)As a user deposits in their balance, we capture the increase in cash and corresponding increase in the user balance.
In-App TransferSender user balance account (decrease)Receiver user balance account (increase)A transfer from user A to user B will deduct the sender balance and increase the receiver's. There is no actual money movement in this transaction, it's simply a ledger mutation.
WithdrawalUser balance account (decrease)Cash account (decrease);
Revenue from fees (increase)
This is the inverse flow to a deposit, with the exception that 2% of the withdrawal would be recognized as a revenue.

Your application code would handle transactions appropriately write them into the ledger according to the guidance presented above. Our team can help you structure your transaction rules in a way that meets your product requirements and optimizes database performance.

Step 3. Defining metadata, categories, and transaction timing

With your chart of accounts and overall transaction schema mapped out, a few important questions remain.

Metadata
Ledgers supports free-form metadata in the form of key-value pairs. Common metadata fields seen amongst digital wallet customers include:

ObjectMetadata
ledgerproductID
ledger accountwalletID; userId; accountType
ledger transactiontransactionType
ledger account categorycategoryType

Ledgers supports querying based on metadata. Any time objects refer to an important concept, it’s helpful to tag them with relevant metadata. By using the list ledger transactions endpoint to query all accounts associated with user wallet #31512, for instance, the system will return all the transactions that modified this account.

Categories
As mentioned before, categories are aggregations at the account level. Categories group multiple accounts. The balance in an account category is equal to the sum of the balances of the accounts within it.

A common category for digital wallets is a user balances grouping, which aggregate the overall balance held in user wallets according to a pre-defined criteria. Read more about categories here.

Transaction timing
Ledgers supports multi-state transactions. Transactions in the ledger can have one of the following status values: pending, posted, or archived.

Digital wallet companies might choose to leave transactions at a pending status until a certain condition is met. For instance: a user may execute a deposit in your digital wallet via ACH but you decide to only take the transaction into account once it clears. You can choose to represent these balances as pending until transactions are finalized, at which point they would become posted. Once posted, they are immutable.

Each account will have one of three balances: posted (sum of all posted entries), pending (sum of posted and pending entries), and available (sum of all posted entries minus outgoing pending entries).

Finally, Ledgers supports back and future dating in the form of an effective_date field on the ledger transaction object. This gives you the flexibility to differentiate between posted dates and effective dates for reporting purposes, as well as include future events in the ledger.

Step 4. Setup your ledger

First, we will make a ledger.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledgers \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "SendCash Ledger",
    "description": "Represents our USD funds and user balances",
  }'

This will return a ledger object with an ID to be used in the following step.

{
    "id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "object": "ledger",
    "name": "SendCash Ledger",
    "description": "Represents our USD funds and user balances",
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:48:05Z",
    "updated_at": "2020-08-04T16:48:05Z"
}

Next, we will create the 3 ledger accounts described above. Note that the bank account is normal_balance=debit (because it represents a balance that we hold), whereas the user wallet accounts are normal_balance=credit (because they represent balances that we owe).

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_accounts \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Cash Account",
    "description": "Tracks our cash",
    "normal_balance": "debit",
    "currency": "USD",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12"
  }'
  
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_accounts \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Jane Doe Wallet",
    "description": "Tracks balance held on behalf of Jane Doe",
    "normal_balance": "credit",
    "currency": "USD",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12"
  }'

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_accounts \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "John Doe Wallet",
    "description": "Tracks balance held on behalf of John Doe",
    "normal_balance": "credit",
    "currency": "USD",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12"
  }'

This will return the 3 ledger accounts you've created.

{
    "id": "f1c7e474-e6d5-4741-9f76-04510c8b6d7a",
    "object": "ledger_account",
    "name": "Cash Account",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "description": "Tracks our cash",
    "normal_balance": "debit",
    "currency": "USD",
    "currency_exponent": 2,
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:54:32Z",
    "updated_at": "2020-08-04T16:54:32Z"
}

{
    "id": "61574fb6-7e8e-403e-980c-ff23e9fbd61b",
    "object": "ledger_account",
    "name": "Jane Doe Wallet",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "description": "Tracks balance held on behalf of Jane Doe",
    "normal_balance": "credit",
    "currency": "USD",
    "currency_exponent": 2,
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:54:32Z",
    "updated_at": "2020-08-04T16:54:32Z"
}

{
    "id": "463237a4-a93d-4396-9be8-5bacb8ceead1",
    "object": "ledger_account",
    "name": "John Doe Wallet",
    "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
    "description": "Tracks balance held on behalf of John Doe",
    "normal_balance": "credit",
    "currency": "USD",
    "currency_exponent": 2,
    "active": true,
    "metadata": {},
    "live_mode": true,
    "created_at": "2020-08-04T16:54:32Z",
    "updated_at": "2020-08-04T16:54:32Z"
}

Step 5. Record transactions

Now that the setup is done, you can write to the ledger to record what happens in your business.

First, we can create a ledger transaction to record that Jane Doe has deposited $100 in her wallet. This will recognize that $100 has entered our bank account, and that $100 of funds are held on behalf of Jane.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_transactions \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Jane Doe cash deposit",
    "effective_date": "2020-08-27",
    "status": "posted",
    "external_id": "97dbb8b1-e6f2-485e-a0ec-6267e3c60718",
    "ledger_entries": [
      {
        "amount": 10000,
        "direction": "debit",
        "ledger_account_id": "f1c7e474-e6d5-4741-9f76-04510c8b6d7a"
      },
      {
        "amount": 10000,
        "direction": "credit",
        "ledger_account_id": "61574fb6-7e8e-403e-980c-ff23e9fbd61b"
      }
    ] 
  }'

Next, Jane uses our app to send $50 to John Doe's wallet. No cash transfer has to occur; we simply record that a portion of the cash that we are holding is owed to a different user. We can create the ledger transaction to record this transfer of funds across accounts.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_transactions \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "Jane Doe wallet transfer to John Doe",
    "effective_date": "2020-08-29",
    "status": "posted",
    "external_id": "c006a6df-72ff-4cbf-aa4a-c18dde4b05c5",
    "ledger_entries": [
      {
        "amount": 5000,
        "direction": "credit",
        "ledger_account_id": "463237a4-a93d-4396-9be8-5bacb8ceead1"
      },
      {
        "amount": 5000,
        "direction": "debit",
        "ledger_account_id": "61574fb6-7e8e-403e-980c-ff23e9fbd61b"
      }
    ]
  }'

Finally, John Doe withdraws $50 from his wallet. We can create a ledger transaction recording that $50 has left our bank account, and that $50 is no longer held on behalf of John.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/ledger_transactions \
  -H 'Content-Type: application/json' \
  -d '{
    "description": "John Doe cash withdrawal",
    "effective_date": "2020-08-30",
    "status": "posted",
    "external_id": "a9a7fd22-8922-498a-b8b3-def4df72d5e4",
    "ledger_entries": [
      {
        "amount": 5000,
        "direction": "credit",
        "ledger_account_id": "f1c7e474-e6d5-4741-9f76-04510c8b6d7a"
      },
      {
        "amount": 5000,
        "direction": "debit",
        "ledger_account_id": "463237a4-a93d-4396-9be8-5bacb8ceead1"
      }
    ]
  }'

Step 6. Read balances

The ledger is a consistent source of truth for your user transactions and balances at scale. For example, you can query the ledger for a user's wallet balance in order to display it in your app.

When Jane wants to see her wallet balance, your app can query the Ledgers API:

curl --request GET \
  -u ORGANIZATION_ID:API_KEY \
   --url https://app.moderntreasury.com/api/ledger_accounts/61574fb6-7e8e-403e-980c-ff23e9fbd61b

This will return Jane's live wallet balance, which you can display in your app.

{
  "id":"61574fb6-7e8e-403e-980c-ff23e9fbd61b",
  "object":"ledger_account",
  "live_mode":true,
  "name":"Jane Doe Wallet",
  "ledger_id": "89c8bd30-e06a-4a79-b396-e6c7e13e7a12",
  "description": "Tracks balance held on behalf of Jane Doe",
  "lock_version":2,
  "normal_balance":"credit",
  "balances":{
    "pending_balance":{
      "credits":10000
      "debits":5000
      "amount":5000
      "currency":"USD",
      "currency_exponent":2
    },
    "posted_balance":{
      "credits":10000
      "debits":5000
      "amount":5000
      "currency":"USD",
      "currency_exponent":2
    }
  },
  "metadata":{},
  "discarded_at":NULL,
  "created_at": "2020-08-04T16:54:32Z",
  "updated_at": "2020-08-04T17:23:12Z"
}