Multi-currency bookkeeping for NodeJS

In the spring of 2022, I started my project Flanker – a bot that can issue a Visa USA card for you with cryptocurrency replenishment.

I needed a simple accounting module, with a light API and no GUI. Pretty soon I found medici – a module running on mongoDB, very lightweight. He lacked multi-currency support, but I decided that it would do for the first time, and implemented it at home.

However, several months passed, the project grew, and I decided to file my bookkeeping with blackjack, multicurrency, and working on my native Sequelize + Postgres bundle.

Module link: Fledger

Why

If you are doing a financial or crypto project that stores financial data, you will need a module that will be responsible for the correct work with financial data.

The correct solution is to implement double ledger accounting from the start.

People who are not familiar with the principles of accounting, it seems that accounting is difficult. Actually it is not, and I will show you why.

I personally saw several projects in which at the start it was decided to make it “simpler”, to introduce some kind of system with one record, which they immediately invented. It was always a shot in the foot. It turns out to be difficult to maintain such a system, it is difficult to migrate from it, it is error-resistant, and so on.

double entry

The principle of double entry in bookkeeping has its roots in the late 15th century, in Italian trading cities.

It reflects a simple truth: money does not come from nowhere, and does not go anywhere, it only flows from pocket to pocket (account to account). Therefore, any movement of money should be reflected (at least) in two accounts – from which one it went, and where it came from. In simple terms, the arrival is called a debit, and the departure is called a credit. The debit must be equal to the credit both at the level of one transaction and at the level of the entire accounting department as a whole. This is the “basic law of accounting”, or balance. It is this simple rule that makes double-entry bookkeeping foolproof.

Usage

Before you start using Fledger, I advise study the READMEbecause minor technical details are not covered here. Here I have focused more on the general principles of accounting.

I will give use cases based on work Flankerbecause this case is indicative in this regard.

Let’s say a user replenishes his account with $100 – deposits 100 USDT to our cryptocurrency wallet. Here is how it can be reflected:

account

Debit

credit

Assets:usdt

100

UserBalances:1

100

Debit – by account Assets:usdt, which reflects the state of our crypto wallet. Credit – by account UserBalances:1which reflects the state of the client’s account from id=1.

This is how this entry would look in the Fledger API:

await book.entry('User 1 deposit')
  .debit('Assets:usdt', 10000, {type: 'userDeposit'})
  .credit('UserBalances:1', 10000, {type: 'userDeposit'})
  .commit()

What you need to pay attention to here:

  • Amounts are entered not in dollars, but in cents. All bookkeeping works in integers, financial data is never stored in real values. I generally store not in cents, but in satoshi (10 ^ -8);

  • each transaction we pass a meta-info object {type: 'userDeposit'}. With the help of meta-information, it is convenient to filter records. For example, now we can from the account Assets:usdt pull all records of user deposits.

Accounts and sub-accounts

Fledger uses the usual accounting concept of subaccounts. For example, we have an account UserBalanceshe has sub-accounts UserBalances:1 and UserBalances:2.

When requesting a balance or account history UserBalances, Fledger will show the balance or history of this account AND all of its sub-accounts. So you can find out the total balance of all users on the site, or, for example, the total balance of all our wallets / bank accounts under the account Assets

Commission

Let’s complicate the example. Let’s say we want to write off a commission from the user immediately upon replenishment. Here’s what it will look like:

Accounts

Debit

credit

Assets:usdt

100

UserBalances:1

95

Income:fees

5

Or in the Fledger API:

await book.entry('User 1 deposit')
  .debit('Assets:usdt', 10000, {type: 'userDeposit'})
  .credit('UserBalances:1', 9500, {type: 'userDeposit'})
  .credit('Income:fees', 500, {type: 'userDeposit'})
  .commit()

One record can contain more than two transactions (in theory – an unlimited number). But they must be in balance!

How to find out where is the debit and where is the credit?

In the example above, we post a debit to the account Assets and account credit UserBalances. Why is it so, and not vice versa?

From the point of view of our company, all accounts in the accounting department can be divided into 4 main types:

I’ll try to explain in a simple way what this means.

Assets – these are the nishtyaki owned by our company. A crypto wallet is definitely an asset, because it is better to have it than not to have it. If the entry shows growth assetsthat is asset account debit (decrease in assets – loan)

Liabilities or Liabilities – it sucks. This is what we owe to the world around us (users, other companies, the state). If the entry shows growth liabilitiesthat is liability account credit (decrease in liabilities – debit).

Now let’s take a look at this entry:

await book.entry('User 1 deposit')
  .debit('Assets:usdt', 10000, {type: 'userDeposit'})
  .credit('UserBalances:1', 9500, {type: 'userDeposit'})
  .credit('Income:fees', 500, {type: 'userDeposit'})
  .commit()

The user replenished our crypto wallet. Is it the growth of our asset? Yes. So this is a debit Assets:usdt.

At the same time, the user replenished the balance of his account in our system (UserBalances:1). Check UserBalances – these are our obligations in front of users. That’s why we credit the account UserBalances:1.

These obligations arose just due to the fact that the user replenished our assets. Do you feel? Here lies another genius of the double-entry method. Assets always equal liabilities! The firm’s assets cannot grow just like that on their own, it is always accompanied by an increase in liabilities. In this case, the balance remains zero!

Incomes. Now let’s deal with the last write transaction: credit('Income:fees', 500, {type: 'userDeposit'}). Check Income:fees this is the income account. We deducted a commission of $5 from the user, our company does not have any obligations in connection with this, but we must record this credit somewhere. For this, you need accounts on which Income is accumulated. At the end of the month, we will look at the credits on this account and see how much we managed to earn (dirty, that is, so far without deducting expenses). Growth parishes – this is income account credit.

Expenses. Let’s say we need to pay for hosting. How does it look from an accounting point of view? We pay money (for example, from our account Assets:bank). This is a decrease in the asset, that is, a credit on the account Assets:bank. And in return we get some intangible asset, that is, a month of hosting. To reflect this fact, there are accounts Expenses. It turns out that in this case we must debit the account Expenses. Growth expenses – this is debit to expense account.

Multicurrency

Suppose we give the opportunity to replenish the account in rubles. Our account, as we remember, is in dollars. A multi-currency transaction occurs. Here’s how to reflect it:

account

Debit

credit

exchange rate

Assets:AlfaBank (RUB)

6000

60.0

UserBalances:1 (USD)

100

1.0

Or in the Fledger API:

await book.entry('User 1 deposit')
  .debit('Assets:AlfaBank ', 6000, {type: 'userDeposit'}, 60.0)
  // курс обмена -----------------------------------------^
  .credit('UserBalances:1', 100, {type: 'userDeposit'})
  .commit()

What to look for here:

  • exchange rate for accounts denominated in foreign currency – a real number;

  • the exchange rate is divisor. That is, to get the corresponding amount in base currency, the amount in foreign currency is divided to the course;

  • debit and credit are balanced here through the exchange rate.

Trading balance account

Let’s say we created an entry with multiple currencies, as in the previous section:

account

Debit

credit

exchange rate

Assets:AlfaBank (RUB)

6000

60.0

UserBalances:1

100

1.0

This record is balanced at the time of its appearance in accounting, and all accounting is balanced (assets are equal to liabilities).

10 minutes passed, the RUBUSD exchange rate changed, became, say, 80.0 (has nothing to do with the real exchange rate!)

Accounting is no longer balanced, rubles have depreciated slightly, and assets no longer equal liabilities. What to do?

The Trading Balance Account comes to the rescue, or the Trade Balance Account. The topic is quite extensive, so for a full review, I suggest going to tutorial by Peter Selinger

But in short, the Trade Balance Account is a special type of account that cannot be used directly. It is multi-currency, and automatically changes its state every time we create a multi-currency operation. It accumulates unbalanced portions of different currencies that have been involved in multi-currency transactions. Thus, if at any time to recalculate its valuation in the base currency (taking into account current exchange rates), it will show a positive or negative balance. A positive balance will mean “gain from currency exchange”, a negative one – “loss from currency exchange”.

In other words, this account at each moment of time forms the necessary offset in order to balance the bookkeeping.

To request Fledger to change the Trading Account for a period of time, there is a method:

// query change of Trading Account state for last month:
let tb = await book.tradingBalance({
  startDate: moment().subtract(1, 'month').toDate(),
  endDate: new Date()
})

// query current state of Trading Account (for all time):
let tb = await book.tradingBalance()

// returns object:
// {
//   base: <change of trading balance converted to base currency, in string>,
//   currency: {
//     USD: <USD trading balance in string>,
//     THB: <THB trading balance in string>
//   }
// }

The balance of this account is not cached, so this request book.tradingBalance() is equivalent to requesting all accounting transactions for all time. Avoid this, it is better to request for limited periods of time.

Outcome

I tried to briefly and clearly outline the very basics of bookkeeping for pogromists, so that it is clear that this does not hurt, and to motivate you to implement it in your own. Of course, you will have to delve a little deeper when you have more complex cases, but it will be much easier to delve into if you have a basic understanding of how accounting works. For ordinary people, this is “smoke and mirrors”, I tried to dispel them.

To make a simple user account system that stores their balances, all you need to do is…:

  • figure out how to record the replenishment of the user’s balance (where is the debit, where is the credit), and how to record his expense;

  • figure out how to get the user’s account balance through the Fledger API (easy);

  • figure out which meta-info is better to add to transactions, so that later it would be convenient to work with them (slicing the issuance of the book.ledger request)

Similar Posts

Leave a Reply Cancel reply