Building a Digital Cash Network
A digital cash network issues balances against reserves held elsewhere, moves them between the parties who hold them, and redeems them back out again. Every one of those events has to be recorded once, in an order every participant agrees on, because the record is what the operator's reserves are reconciled against and what a regulator will ask to see.
On this page
In this guide, we model such a network: an operator issuing two currencies, holders paying each other and paying merchants, currency exchange at the point of payment, and redemption back to the banking system.
OverviewLink to this section
In our example network there are two kinds of participant: holders and merchants. These will each be represented as accounts in the ledger.
There are two currencies, USD and EUR, and a scheme-funded rebate. These will each be represented as assets in the ledger. (This can be extended to any number of currencies.)
Currency is issued into a holder's account when they fund it, transferred between holders, paid to merchants, and retired when it is redeemed. Merchants that take part in the rebate scheme cause a rebate to be issued to the holder at the moment of payment. All of these interactions will be represented as transactions in the ledger.
SetupLink to this section
To set up our ledger, we will create several keys, assets, and accounts.
KeysLink to this section
Authority to create transactions in the ledger is assigned to four distinct systems:
- Treasury - responsible for processing deposits and withdrawals, collecting fees from merchants, and performing currency exchange
- Holder - responsible for managing withdrawals and transfers from holder accounts
- Merchant - responsible for managing withdrawals from merchant accounts
- Scheme - responsible for issuing rebates
Each system will have a key that will be used to perform its actions in the ledger. To create those keys, we run the following:
new Key.Builder()
.setId("treasury")
.create(ledger);
new Key.Builder()
.setId("holder")
.create(ledger);
new Key.Builder()
.setId("merchant")
.create(ledger);
new Key.Builder()
.setId("scheme")
.create(ledger);ledger.keys.create({id: 'treasury'})
ledger.keys.create({id: 'holder'})
ledger.keys.create({id: 'merchant'})
ledger.keys.create({id: 'scheme'})ledger.keys.create(id: 'treasury')
ledger.keys.create(id: 'holder')
ledger.keys.create(id: 'merchant')
ledger.keys.create(id: 'scheme')AssetsLink to this section
Assets represent the different kinds of balance a merchant or holder account can carry. Our ledger will have assets for USD, EUR, and rebates.
First, we create the currency assets using the treasury key:
new Asset.Builder()
.setId("usd")
.addKeyId("treasury")
.addTag("type", "currency")
.create(ledger);
new Asset.Builder()
.setId("eur")
.addKeyId("treasury")
.addTag("type", "currency")
.create(ledger);ledger.assets.create({
id: 'usd',
keyIds: ['treasury'],
tags: {type: 'currency'}
})
ledger.assets.create({
id: 'eur',
keyIds: ['treasury'],
tags: {type: 'currency'}
})ledger.assets.create(
id: 'usd',
key_ids: ['treasury'],
tags: {type: 'currency'}
)
ledger.assets.create(
id: 'eur',
key_ids: ['treasury'],
tags: {type: 'currency'}
)Next, we create the rebate asset using the scheme key. It has a key of its own because the authority to issue a rebate is not the authority to issue money:
new Asset.Builder()
.setId("rebate")
.addKeyId("scheme")
.create(ledger);ledger.assets.create({
id: 'rebate',
keyIds: ['scheme']
})ledger.assets.create(
id: 'rebate',
key_ids: ['scheme']
)AccountsLink to this section
Each holder and each merchant needs an account in the ledger. Although these accounts would be created by the network as participants are onboarded, for this example we'll assume we have two holders and two merchants and create them as part of the setup. We also need the operator's own account.
We will use tags to differentiate between the types of accounts.
First, we create the holder accounts using the holder key:
new Account.Builder()
.setId("alice")
.addKeyId("holder")
.addTag("type", "holder")
.create(ledger);
new Account.Builder()
.setId("bob")
.addKeyId("holder")
.addTag("type", "holder")
.create(ledger);ledger.accounts.create({
id: 'alice',
keyIds: ['holder'],
tags: {type: 'holder'}
})
ledger.accounts.create({
id: 'bob',
keyIds: ['holder'],
tags: {type: 'holder'}
})ledger.accounts.create(
id: 'alice',
key_ids: ['holder'],
tags: {type: 'holder'}
)
ledger.accounts.create(
id: 'bob',
key_ids: ['holder'],
tags: {type: 'holder'}
)Next, we create the merchant accounts using the merchant key:
new Account.Builder()
.setId("merchant1")
.addKeyId("merchant")
.addTag("type", "merchant")
.create(ledger);
new Account.Builder()
.setId("merchant2")
.addKeyId("merchant")
.addTag("type", "merchant")
.create(ledger);ledger.accounts.create({
id: 'merchant1',
keyIds: ['merchant'],
tags: {type: 'merchant'}
})
ledger.accounts.create({
id: 'merchant2',
keyIds: ['merchant'],
tags: {type: 'merchant'}
})ledger.accounts.create(
id: 'merchant1',
key_ids: ['merchant'],
tags: {type: 'merchant'}
)
ledger.accounts.create(
id: 'merchant2',
key_ids: ['merchant'],
tags: {type: 'merchant'}
)Finally, we create the operator account using the treasury key:
new Account.Builder()
.setId("operator")
.addKeyId("treasury")
.addTag("type", "operator")
.create(ledger);ledger.accounts.create({
id: 'operator',
keyIds: ['treasury'],
tags: {type: 'operator'}
})ledger.accounts.create(
id: 'operator',
key_ids: ['treasury'],
tags: {type: 'operator'}
)Transaction TypesLink to this section
Now that we have created our assets and accounts, we can model the different types of transactions.
DepositLink to this section
When a holder funds their account, we create a transaction containing an issue action to issue the amount received into their account. This will create a number of tokens of the corresponding asset and put them in the account.
We can use action tags to record details about the deposit, such as the deposit method and associated transaction ID in an external system.
For this example, we assume that Alice funds her account with $100.00 by ACH. Note that the amount issued is 10000, because the fundamental unit of the USD asset is a cent.
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Issue()
.setAssetId("usd")
.setAmount(10000)
.setDestinationAccountId("alice")
.addActionTagsField("type", "deposit")
.addActionTagsField("system", "ach")
.addActionTagsField("ach_transaction_id", "11111")
).transact(ledger);ledger.transactions.transact(builder => {
builder.issue({
assetId: 'usd',
amount: 10000,
destinationAccountId: 'alice',
actionTags: {
type: 'deposit',
system: 'ach',
ach_transaction_id: '11111'
}
})
})ledger.transactions.transact do |builder|
builder.issue(
asset_id: 'usd',
amount: 10000,
destination_account_id: 'alice',
action_tags: {
type: 'deposit',
system: 'ach',
ach_transaction_id: '11111'
}
)
endSince this transaction issues USD tokens, it must be signed by the treasury key. This is handled automatically by the transact SDK method (because we associated that key with the USD asset).
P2P Payment (holder-to-holder)Link to this section
When a holder transfers money to another holder, we create a transaction containing a transfer action to transfer the amount of the requested currency from the sender to the recipient.
We can use action tags to record the reason for the transfer.
For this example, we assume that Alice transfers $25.50 to Bob.
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Transfer()
.setAssetId("usd")
.setAmount(2550)
.setSourceAccountId("alice")
.setDestinationAccountId("bob")
.addActionTagsField("type", "p2p_payment")
).transact(ledger);ledger.transactions.transact(builder => {
builder.transfer({
assetId: 'usd',
amount: 2550,
sourceAccountId: 'alice',
destinationAccountId: 'bob',
actionTags: {type: 'p2p_payment'}
})
})ledger.transactions.transact do |builder|
builder.transfer(
asset_id: 'usd',
amount: 2550,
source_account_id: 'alice',
destination_account_id: 'bob',
action_tags: {type: 'p2p_payment'}
)
endBecause this transaction transfers from Alice's account, it must be signed by the holder key. This is handled automatically by the transact SDK method.
Merchant PaymentLink to this section
When a holder pays a merchant, the operator keeps a portion as a fee, and the scheme credits the holder with a rebate.
All three belong to the same event, so we model them as a single atomic transaction with three actions:
- Transfer - the payment, from holder to merchant
- Retire - the fee, from the merchant
- Issue - the rebate, to the holder
For this example, we will assume a $10 payment from Alice to Merchant 1, a fee rate of 2%, and a rebate of one unit per cent spent.
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Transfer()
.setAssetId("usd")
.setAmount(1000)
.setSourceAccountId("alice")
.setDestinationAccountId("merchant1")
.addActionTagsField("type", "merchant_payment")
).addAction(new Transaction.Builder.Action.Retire()
.setAssetId("usd")
.setAmount(20)
.setSourceAccountId("merchant1")
.addActionTagsField("type", "operator_fee")
).addAction(new Transaction.Builder.Action.Issue()
.setAssetId("rebate")
.setAmount(1000)
.setDestinationAccountId("alice")
.addActionTagsField("type", "rebate_earned")
).transact(ledger);ledger.transactions.transact(builder => {
builder.transfer({
assetId: 'usd',
amount: 1000,
sourceAccountId: 'alice',
destinationAccountId: 'merchant1',
actionTags: {type: 'merchant_payment'}
})
builder.retire({
assetId: 'usd',
amount: 20,
sourceAccountId: 'merchant1',
actionTags: {type: 'operator_fee'}
})
builder.issue({
assetId: 'rebate',
amount: 1000,
destinationAccountId: 'alice',
actionTags: {type: 'rebate_earned'}
})
})ledger.transactions.transact do |builder|
builder.transfer(
asset_id: 'usd',
amount: 1000,
source_account_id: 'alice',
destination_account_id: 'merchant1',
action_tags: {type: 'merchant_payment'}
)
builder.retire(
asset_id: 'usd',
amount: 20,
source_account_id: 'merchant1',
action_tags: {type: 'operator_fee'}
)
builder.issue(
asset_id: 'rebate',
amount: 1000,
destination_account_id: 'alice',
action_tags: {type: 'rebate_earned'}
)
endBecause this transaction transfers from Alice's account, transfers from Merchant 1's account, and issues rebates, it must be signed by the holder key, the merchant key, and the scheme key. This is handled automatically by the transact SDK method.
Note that instead of retiring the fee, we could have transferred it to the operator's account. That design, however, would leave those tokens sitting there indefinitely. In general, you should only use tokens for amounts that will be needed later. In other words, you should use balances of tokens for current state, and query actions for historical state. With our design, we can still determine the total amount of fees collected by querying actions with the type of operator_fee. See the Queries section for an example of this query.
Merchant FX (foreign exchange) PaymentLink to this section
If a holder carries one currency and a merchant accepts another, the currency has to be exchanged. The operator acts as the counterparty to that exchange, inside the same transaction as the payment.
We model this currency exchange as a single atomic transaction with two actions:
- Transfer - payment amount of holder currency from holder to operator
- Transfer - converted amount (based on operator fx rate) of merchant currency from operator to merchant
The currency exchange rate would be determined by the operator at the time of transaction, and the holder would be presented with the amount of their currency required to pay the amount of the merchant's currency.
Once the currency is exchanged, we finish the transaction the same way as the previous example.
- Retire - fee amount of merchant's currency, from the merchant
- Issue - the rebate earned, to the holder
In this example, we assume that Alice needs to pay 20.00 EUR to Merchant 2, which the operator can provide for 26.50 USD. Additionally, the operator will take a fee of 0.40 EUR from Merchant 2 (2% of the payment amount in EUR) and Alice will earn one rebate unit per cent spent in USD.
The operator needs enough EUR in its own account to settle the payment, so we first issue some EUR to it.
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Issue()
.setAssetId("eur")
.setAmount(50000)
.setDestinationAccountId("operator")
.addActionTagsField("type", "fx_deposit")
).transact(ledger);ledger.transactions.transact(builder => {
builder.issue({
assetId: 'eur',
amount: 50000,
destinationAccountId: 'operator',
actionTags: {type: 'fx_deposit'}
})
})ledger.transactions.transact do |builder|
builder.issue(
asset_id: 'eur',
amount: 50000,
destination_account_id: 'operator',
action_tags: {type: 'fx_deposit'}
)
endNow that the operator account has enough EUR, we can proceed with our FX payment.
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Transfer()
.setAssetId("usd")
.setAmount(2650)
.setSourceAccountId("alice")
.setDestinationAccountId("operator")
.addActionTagsField("type", "merchant_payment")
.addActionTagsField("sub_type", "fx_payment")
).addAction(new Transaction.Builder.Action.Transfer()
.setAssetId("eur")
.setAmount(2000)
.setSourceAccountId("operator")
.setDestinationAccountId("merchant2")
.addActionTagsField("type", "fx_payment")
).addAction(new Transaction.Builder.Action.Retire()
.setAssetId("eur")
.setAmount(40)
.setSourceAccountId("merchant2")
.addActionTagsField("type", "operator_fee")
).addAction(new Transaction.Builder.Action.Issue()
.setAssetId("rebate")
.setAmount(2650)
.setDestinationAccountId("alice")
.addActionTagsField("type", "rebate_earned")
).transact(ledger);ledger.transactions.transact(builder => {
builder.transfer({
assetId: 'usd',
amount: 2650,
sourceAccountId: 'alice',
destinationAccountId: 'operator',
actionTags: {
type: 'merchant_payment',
sub_type: 'fx_payment'
}
})
builder.transfer({
assetId: 'eur',
amount: 2000,
sourceAccountId: 'operator',
destinationAccountId: 'merchant2',
actionTags: {type: 'fx_payment'}
})
builder.retire({
assetId: 'eur',
amount: 40,
sourceAccountId: 'merchant2',
actionTags: {type: 'operator_fee'}
})
builder.issue({
assetId: 'rebate',
amount: 2650,
destinationAccountId: 'alice',
actionTags: {type: 'rebate_earned'}
})
})ledger.transactions.transact do |builder|
builder.transfer(
asset_id: 'usd',
amount: 2650,
source_account_id: 'alice',
destination_account_id: 'operator',
action_tags: {
type: 'merchant_payment',
sub_type: 'fx_payment'
}
)
builder.transfer(
asset_id: 'eur',
amount: 2000,
source_account_id: 'operator',
destination_account_id: 'merchant2',
action_tags: {type: 'fx_payment'}
)
builder.retire(
asset_id: 'eur',
amount: 40,
source_account_id: 'merchant2',
action_tags: {type: 'operator_fee'}
)
builder.issue(
asset_id: 'rebate',
amount: 2650,
destination_account_id: 'alice',
action_tags: {type: 'rebate_earned'}
)
endSince this transaction transfers from Alice's account, transfers from the operator's account, transfers from Merchant 2's account, and issues rebates, it must be signed by the holder key, the treasury key, the merchant key, and the scheme key. This is handled automatically by the transact SDK method.
WithdrawalLink to this section
When a holder or merchant redeems a balance back to the banking system, we create a transaction containing a retire action to retire that amount from their account. The money leaves the network, so the tokens representing it stop existing.
We can use action tags to record details about the withdrawal, such as the withdrawal method and associated transaction ID in an external system.
For this example, we'll assume that Merchant1 redeems $5 by ACH.
new Transaction.Builder()
.addAction(new Transaction.Builder.Action.Retire()
.setAssetId("usd")
.setAmount(500)
.setSourceAccountId("merchant1")
.addActionTagsField("type", "withdrawal")
.addActionTagsField("system", "ach")
.addActionTagsField("ach_transaction_id", "22222")
).transact(ledger);ledger.transactions.transact(builder => {
builder.retire({
assetId: 'usd',
amount: 500,
sourceAccountId: 'merchant1',
actionTags: {
type: 'withdrawal',
system: 'ach',
ach_transaction_id: '22222'
}
})
})ledger.transactions.transact do |builder|
builder.retire(
asset_id: 'usd',
amount: 500,
source_account_id: 'merchant1',
action_tags: {
type: 'withdrawal',
system: 'ach',
ach_transaction_id: '22222'
}
)
endSince this transaction retires from a merchant account, it must be signed by the merchant key. This is handled automatically by the transact SDK method.
QueriesLink to this section
Now that we have created several transactions, we can query the ledger in various ways.
Balances in an AccountLink to this section
If we want to know the balances of different assets in an account, we perform a sum tokens query, filtering to the account id and summing the results by asset id.
For example, let's list the balances in Alice's account.
TokenSum.ItemIterable sums = new Token.SumBuilder()
.setFilter("accountId=$1")
.addFilterParameter("alice")
.addGroupByField("assetId")
.getIterable(ledger);
for (TokenSum sum : sums) {
System.out.println("amount: " + sum.amount );
System.out.println("Asset: " + sum.assetId);
System.out.println("");
}var page1 = ledger.tokens.sum({
filter: 'accountId=$1',
filterParams: ['alice'],
groupBy: ['assetId']
}).page()
page1.items.forEach(sum => {
console.log('amount: ' + sum.amount)
console.log('asset: ' + sum.assetId)
console.log('')
})ledger.tokens.sum(
filter: 'account_id=$1',
filter_params: ['alice'],
group_by: ['asset_id']
).each do |sum|
puts 'amount: ' + sum.amount.to_s
puts 'asset: ' + sum.asset_id
puts ''
endwhich will output:
amount: x
asset: usd
amount: y
asset: rebateTotal Amount of Tokens in the LedgerLink to this section
If we want to know the total amount of each asset in the ledger across all accounts (merchants, holders, and operator), we perform a sum tokens query (with no filter) and group by asset id.
sums = new Token.SumBuilder()
.addGroupByField("assetId")
.getIterable(ledger);
for (TokenSum sum : sums) {
System.out.println("amount: " + sum.amount);
System.out.println("Asset: " + sum.assetId);
System.out.println("");
}page1 = ledger.tokens.sum({
groupBy: ['assetId']
}).page()
page1.items.forEach(sum => {
console.log('amount: ' + sum.amount)
console.log('asset: ' + sum.assetId)
console.log('')
})ledger.tokens.sum(
group_by: ['asset_id']
).each do |sum|
puts 'amount: ' + sum.amount.to_s
puts 'asset: ' + sum.asset_id
puts ''
endwhich will output:
amount: 100
asset: usdAmount of USD in Each Type of AccountLink to this section
If we want to know the amount of USD in each type of account (merchants, holders, and operator), we perform a sum tokens query, filtering to the USD asset and group the results by type account tag.
TokenSum.ItemIterable sums = new Token.SumBuilder()
.setFilter("assetId=$1")
.addFilterParameter("usd")
.addGroupByField("accountTags.type")
.getIterable(ledger);
for (TokenSum sum : sums) {
System.out.println("amount: " + sum.amount);
System.out.println("account type: " + sum.accountTags.get("type"));
System.out.println("");
}page1 = ledger.tokens.sum({
filter: 'assetId=$1',
filterParams: ['usd'],
groupBy: ['accountTags.type']
}).page()
page1.items.forEach(sum => {
console.log('amount: ' + sum.amount)
console.log('account type: ' + sum.accountTags.type)
console.log('')
})ledger.tokens.sum(
filter: 'asset_id=$1',
filter_params: ['usd'],
group_by: ['account_tags.type']
).each do |sum|
puts 'amount: ' + sum.amount.to_s
puts 'account type: ' + sum.account_tags['type']
puts ''
endwhich will output:
amount: x
account type: holder
amount: y
account type: merchant
amount: z
account type: operatorTotal FeesLink to this section
If we want to know the total fees that have been collected, we perform a sum actions query, filtering to actions with the operator_fee type in tags.
ActionSum.ItemIterable sums = new Action.SumBuilder()
.setFilter("tags.type=$1")
.addFilterParameter("operator_fee")
.getIterable(ledger);
for (ActionSum sum : sums) {
System.out.println("total fees: " + sum.amount);
System.out.println("");
}page1 = ledger.actions.sum({
filter: 'tags.type=$1',
filterParams: ['operator_fee']
}).page()
page1.items.forEach(sum => {
console.log('total fees: ' + sum.amount)
console.log('')
})ledger.actions.sum(
filter: 'tags.type=$1',
filter_params: ['operator_fee']
).each do |sum|
puts 'total fees: ' + sum.amount.to_s
puts ''
endwhich will output:
total fees: ...Recent Actions in an AccountLink to this section
If we want to display the latest actions for a specific account, we perform a list actions query, filtering to actions in which the account was the source or destination.
Let's query for actions that involved Alice's account and display the type of action by accessing the type field in the action tags. Note that we can set the page size to control how many actions are returned (e.g., receive the 10 most recent).
Action.Page actions = new Action.ListBuilder()
.setFilter("sourceAccountId=$1 OR destinationAccountId=$1")
.addFilterParameter("alice")
.setPageSize(10)
.getPage(ledger);
for (Action action : actions.items) {
String source = "n/a";
String destination = "n/a";
if (action.sourceAccountId != null) {
source = action.sourceAccountId;
}
if (action.destinationAccountId != null) {
destination = action.destinationAccountId;
}
System.out.println("type: " + action.tags.get("type"));
System.out.println("asset: " + action.assetId);
System.out.println("amount: " + action.amount);
System.out.println("from: " + source);
System.out.println("to: " + destination);
System.out.println("");
}page1 = ledger.actions.list({
filter: 'sourceAccountId=$1 OR destinationAccountId=$1',
filterParams: ['alice']
}).page({size: 10})
page1.items.forEach(action => {
const source = action.sourceAccountId ? action.sourceAccountId : 'n/a'
const destination = action.destinationAccountId ? action.destinationAccountId : 'n/a'
console.log('type: ' + action.tags.type)
console.log('asset: ' + action.assetId)
console.log('amount: ' + action.amount)
console.log('from: ' + source)
console.log('to: ' + destination)
console.log('')
})page1 = ledger.actions.list(
filter: 'source_account_id=$1 OR destination_account_id=$1',
filter_params: ['alice']
).page(size: 10)
page1.each do |action|
source = 'n/a'
destination = 'n/a'
source = action.source_account_id if action.source_account_id
destination = action.destination_account_id if action.destination_account_id
puts 'type: ' + action.tags['type']
puts 'asset: ' + action.asset_id
puts 'amount: ' + action.amount.to_s
puts 'from: ' + source
puts 'to: ' + destination
puts ''
endwhich will output:
type: merchant_payment
asset: usd
amount: ...
from: ...
to: ...
type: p2p_payment
asset: usd
amount: ...
from: ...
to: ...