A multi-currency wallet system is a system that manages balances and transactions between 2 or more asset classes. In this example, you’ll learn how to build a simple ledger that manages transaction workflows between multiple currencies.

We’ll learn about:

  1. Defining and creating your ledger structure
  2. Balance creation
  3. Moving money into wallets
  4. Moving money out of wallets
  5. Moving money between multicurrency wallets
  6. Best practices

1. Ledger structure

The entry point of the Blnk ledger system is ledger folders. These folders serve as a way to group and manage assets, accounts, and balances that fit your product or organization’s structure.

In this guide, we’ll use a simple structure:

  • USD Ledger: Contains all USD wallets
  • EUR Ledger: Contains all EUR wallets

The ledger structure is flexible and can be customized based on your specific needs. For instance, you could group by users instead of currencies, or use a combination of both.

See also:

1

Creating a USD Ledger

View your ledgers in your terminal:

bash
blnk ledgers list

Always store the ledger_id in your database. You’ll need it for future operations related to this ledger.

2

Creating a EUR Ledger

It is the same process as the USD ledger creation. Only change the name of the ledger and adjust the meta_data as needed.

2. Balance (wallet) creation

Blnk uses the concept of ledger balances to manage accounts/balances in a ledger. In our example, we’ll create wallets for a customer named Jerry, who will have both a USD and EUR wallet.

See also:

1

Creating a USD Wallet for Customer A

View your balances in your terminal:

bash
blnk balances list

The balance_id is important. Always store this in your database and associate it with the customer. You’ll use this id for all future transactions involving this wallet.

2

Creating a EUR Wallet for Customer A

It is the same process as the USD wallet creation. Only change the ledger_id to the EUR ledger ID and set the currency to “EUR”.

3. Moving money into wallets

Once ledgers are created and balances/wallets have been set up for your customers, the next step is to start recording transactions.

In this example, we’ll cover funding our wallets with actual cash received from the outside world or other apps (banks/wallets etc). This section introduces key concepts in Blnk like:

See also:

1

Funding Customer A's USD Wallet with $200.00

FieldDescription
amountThe actual amount received, in this case 200.00 USD.
precisionBlnk uses the concept of precision to accurately manage and store float amounts. For accuracy, always pass a precision to convert the original amount to its lowest denominator (without floats). In this case, to convert USD to cents, we use a precision of 100. So, 200.00 USD * 100 = 20000 cents.
sourceThe source is part of Blnk’s implementation of the double-entry accounting principle. In this example, the amount was debited from the outside world (sender) and credited to one of our internal wallets. We use the concept of general ledgers to keep a record of all money coming from the outside world into our ledger. To easily do this without having to create a new ledger folder called general ledger (Blnk does this automatically), you can use the @ prefix followed by what you want to name the general ledger balance. In this case, it’s @World. You can name it anything, for example, @external-service-partner if you are receiving the payment from an external partner and want to track how much you’ve received or sent to the partner.
destinationIn this example, the destination is Customer A’s USD wallet, which we pass using the balance_id obtained when the balance was created: bln_e39a239a-a6ca-4509-b0d9-29dcc5630f8a.
allow_overdraftSince we’re tracking the general ledger balances (in this case, @World), we’re essentially moving 200.00 USD from the @World balance to Customer A’s USD balance. But since in our application the @World balance is empty initially, we want to force an overdraft so the transaction goes through. If overdraft is not true, the transaction will be rejected with insufficient funds because the @World general ledger balance is empty at the beginning. Setting overdraft to true enables us to debit past the balance.
meta_dataThis field allows you to store additional information about the transaction. It’s crucial for reconciliation and auditing purposes.

View your transactions in your terminal:

bash
blnk transactions list

Always use the precision field to avoid floating-point arithmetic issues. For USD and EUR, a precision of 100 (representing cents) is typically used.

2

Funding Customer A's EUR Wallet with 3500.50 EUR

The process is similar to funding the USD wallet. Here’s the key part of the request body.

NodeJS
body: JSON.stringify({
    "amount": 3500.5,
    "precision": 100,
    "reference": "ref-0-02",
    "description": "invoice A fulfilled",
    "currency": "EUR",
    "source": "@World",
    "destination": "bln_f945f959-bebf-4764-ab2d-2ae194c1b93e",
    "allow_overdraft": true,
    "meta_data": {
        "sender_name": "Nlnk Bank",
        "sender_internal_id": "563825"
    }
})

The principles explained for the USD transaction apply here as well. Always ensure you’re using the correct precision and currency for each transaction.

4. Moving money out of wallets

Once the ledgers are created and balances/wallets have been set up for your customers, and the wallets have been funded, you can record transactions that move money out of the wallets.

See also:

1

Transferring $70.32 from Customer A's USD Wallet

FieldDescription
amountThe amount to be transferred, in this case 70.32 USD.
precisionAs before, we use 100 to represent cents.
sourceThis time, we’re transferring from the customer’s wallet, so we use the balance_id of their USD wallet.
destinationWe’re transferring to the outside world, so we use “@World”.
allow_overdraftThis is set to false because we don’t want to allow the customer’s balance to go negative.

Always check the balance after a transaction to ensure it’s updated correctly. Here’s how you might do that.

NodeJS
const checkBalance = (balanceId) => {
const options = {
    method: 'GET',
    url: `http://localhost:5001/balances/${balanceId}`,
    headers: {
    'X-Blnk-Key': 'blnk-api',
    'Content-Type': 'application/json'
    }
};

request(options, (error, response) => {
    if (error) throw new Error(error);
    console.log('Updated balance:', JSON.parse(response.body).balance);
});
};

// Use it after a transaction
checkBalance('bln_e39a239a-a6ca-4509-b0d9-29dcc5630f8a');

Balance after the transaction:

Response
{
    "balance": 12969,
    "version": 3,
    "inflight_balance": 0,
    "credit_balance": 20000,
    "inflight_credit_balance": 0,
    "debit_balance": 7031,
    "inflight_debit_balance": 0,
    "precision": 0,
    "ledger_id": "ldg_5dff0196-11f6-4674-87a2-cf3e39bd20d2",
    "identity_id": "",
    "balance_id": "bln_e39a239a-a6ca-4509-b0d9-29dcc5630f8a",
    "indicator": "",
    "currency": "USD",
    "created_at": "2024-07-05T08:13:18.882616Z",
    "inflight_expires_at": "0001-01-01T00:00:00Z",
    "meta_data": null
}

Note that the balance has decreased by 7031 cents (70.31 USD), which matches our transaction amount.

2

Transferring 1470.49 EUR from Customer A's EUR Wallet

The process is similar to the USD transfer. Here’s the key part of the request body:

NodeJS
body: JSON.stringify({
"amount": 1470.49,
"precision": 100,
"reference": "ref-0-05",
"description": "payment for new iphone",
"currency": "EUR",
"source": "bln_f945f959-bebf-4764-ab2d-2ae194c1b93e",
"destination": "@world"
})

Always ensure you’re using the correct balance_id for the source wallet and the correct currency for each transaction.

5. Moving money between multicurrency wallets

Blnk supports moving money between balances of different currencies. This feature is crucial for applications dealing with multiple currencies.

See also:

Converting $100 USD to EUR (at a rate of 1 USD = 0.92 EUR)

FieldDescription
amountThe amount to be converted, in this case 100 USD.
precisionAs before, we use 100 to represent cents.
sourceThe balance_id of the USD wallet.
destinationThe balance_id of the EUR wallet.
rateThe conversion rate from USD to EUR. In this case, 1 USD = 0.92 EUR.
meta_dataIt’s crucial to store information about the rate used and its source for auditing purposes.

Always store the conversion rate and its source in the meta_data. This is crucial for auditing and reconciliation.

After this transaction, you should check both the USD and EUR balances

USD Balance after the transaction:

Response
{
    "balance": 2969,
    "version": 4,
    "inflight_balance": 0,
    "credit_balance": 20000,
    "inflight_credit_balance": 0,
    "debit_balance": 17031,
    "inflight_debit_balance": 0,
    "precision": 0,
    "ledger_id": "ldg_5dff0196-11f6-4674-87a2-cf3e39bd20d2",
    "identity_id": "",
    "balance_id": "bln_e39a239a-a6ca-4509-b0d9-29dcc5630f8a",
    "indicator": "",
    "currency": "USD",
    "created_at": "2024-07-05T08:13:18.882616Z",
    "inflight_expires_at": "0001-01-01T00:00:00Z",
    "meta_data": null
}

EUR Balance after the transaction:

Response
{
    "balance": 212201,
    "version": 4,
    "inflight_balance": 0,
    "credit_balance": 359250,
    "inflight_credit_balance": 0,
    "debit_balance": 147049,
    "inflight_debit_balance": 0,
    "precision": 0,
    "ledger_id": "ldg_4b53fb9a-bec7-423b-bfc5-106c6241131f",
    "identity_id": "",
    "balance_id": "bln_f945f959-bebf-4764-ab2d-2ae194c1b93e",
    "indicator": "",
    "currency": "EUR",
    "created_at": "2024-07-05T08:13:58.533434Z",
    "inflight_expires_at": "0001-01-01T00:00:00Z",
    "meta_data": null
}

Note that the USD balance has decreased by 10000 cents (100 USD), and the EUR balance has increased by 9200 cents (92 EUR), which matches our conversion rate.

See also

Managing side effects with Inflight

A deep-dive guide into how to implement Inflight in your application.

Best practices

  1. Immediate Action: Process refunds as soon as you receive notification of a failed verification to ensure good customer experience.
  2. Detailed Logging: Always include detailed information in the meta_data field. This aids in troubleshooting and auditing.
  3. Balance Verification: Always verify the balance after processing a refund to ensure the transaction was successful.
  4. Error Handling: Implement robust error handling in your refund process. If a refund fails, you may need to retry or escalate to manual intervention.
  5. Customer Communication: Implement a system to notify customers about the failed transaction and subsequent refund.
  6. Reconciliation: Regularly reconcile your internal records with Blnk’s transaction and refund logs to ensure accuracy.
  7. Verify Webhook Authenticity: In a production environment, implement a mechanism to verify that the webhook is genuinely from your payment provider. This often involves checking a signature or secret key.
  8. Idempotency: Ensure your webhook handler is idempotent. Providers may send the same webhook multiple times, so your system should handle duplicate notifications gracefully.
  9. Asynchronous Processing: For high-volume systems, consider processing webhooks asynchronously. You can acknowledge receipt immediately and process the webhook contents in a background job.
  10. Monitoring: Set up monitoring and alerting for your webhook endpoint. This can help you quickly identify and respond to any issues in the payment verification and refund process.

Need help?

We are very happy to help you make the most of Blnk, regardless of whether it is your first time or you are switching from another tool.

To ask questions or discuss issues, please join our Discord community.