Examples

Building a Tokenized Securities Platform

A venue that holds tokenized securities and cash on behalf of its participants needs one record of who owns what, and a settlement step that either happens completely or not at all. Every deposit, trade, transfer and withdrawal is a transaction, ordered and timestamped, and a trade that moves a security one way and cash the other is a single transaction that cannot be half-applied.

On this page

In this guide, we model such a platform: investors depositing cash and securities, trades settling between them against cash or against each other, sales to counterparties outside the platform, and withdrawals net of a fee.

OverviewLink to this section

In our example platform, each investor is represented as an account in the ledger.

There are two currencies, USD and EUR, and two tokenized securities, ACME (common stock in Acme Corp) and ZETA (common stock in Zeta Industries). These will each be represented as assets in the ledger. (This can be extended to any number of currencies and instruments.)

Cash and securities can be deposited, withdrawn, and traded between investors. The operator charges a 1% fee on all withdrawals of cash. 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 two distinct systems:

  1. Treasury - responsible for deposits
  2. Exchange - responsible for settling trades between investors, and for withdrawals

Each system will have a key that will be used to perform their actions in the ledger. Splitting them means the service that settles trades holds no authority to bring new cash or new securities into existence.

To create these keys, we run the following:

new Key.Builder()
  .setId("treasury")
  .create(ledger);

new Key.Builder()
  .setId("exchange")
  .create(ledger);
ledger.keys.create({id: 'treasury'})
ledger.keys.create({id: 'exchange'})
ledger.keys.create(id: 'treasury')
ledger.keys.create(id: 'exchange')

AssetsLink to this section

Assets represent the different types of holding an investor can have. We will create assets for USD, EUR, ACME, and ZETA, all 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);

new Asset.Builder()
  .setId("acme")
  .addKeyId("treasury")
  .addTag("type", "security")
  .create(ledger);

new Asset.Builder()
  .setId("zeta")
  .addKeyId("treasury")
  .addTag("type", "security")
  .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: 'acme',
  keyIds: ['treasury'],
  tags: {type: 'security'}
})

ledger.assets.create({
  id: 'zeta',
  keyIds: ['treasury'],
  tags: {type: 'security'}
})
ledger.assets.create(
  id: 'usd',
  key_ids: ['treasury'],
  tags: {type: 'currency'}
)

ledger.assets.create(
  id: 'eur',
  key_ids: ['treasury'],
  tags: {type: 'currency'}
)

ledger.assets.create(
  id: 'acme',
  key_ids: ['treasury'],
  tags: {type: 'security'}
)

ledger.assets.create(
  id: 'zeta',
  key_ids: ['treasury'],
  tags: {type: 'security'}
)

AccountsLink to this section

We will need an account in the ledger for each investor. Although these accounts would be created by the platform as investors are onboarded, for this example we'll assume we have two (Alice and Bob) and create them as part of the setup.

We will use tags to differentiate between the types of accounts.

We use the exchange key to create all accounts.

new Account.Builder()
  .setId("alice")
  .addKeyId("exchange")
  .addTag("type", "investor")
  .create(ledger);

new Account.Builder()
  .setId("bob")
  .addKeyId("exchange")
  .addTag("type", "investor")
  .create(ledger);
ledger.accounts.create({
  id: 'alice',
  keyIds: ['exchange'],
  tags: {type: 'investor'}
})

ledger.accounts.create({
  id: 'bob',
  keyIds: ['exchange'],
  tags: {type: 'investor'}
})
ledger.accounts.create(
  id: 'alice',
  key_ids: ['exchange'],
  tags: {type: 'investor'}
)

ledger.accounts.create(
  id: 'bob',
  key_ids: ['exchange'],
  tags: {type: 'investor'}
)

Transaction TypesLink to this section

Now that we have created our assets and accounts, we track events with transactions. A single transaction can include multiple actions, involving any number of assets and accounts. The actions in a transaction occur simultaneously, as a single, atomic operation. A transaction can never be partially applied.

DepositLink to this section

When an investor funds their account with cash, or deposits securities into custody, we create a transaction containing an issue action for the amount deposited. This creates a number of tokens of the corresponding asset and puts them in the account.

We can use action tags to record details about the deposit, such as the deposit method and the associated transaction ID in an external system — the reference an auditor will use to tie a ledger entry back to the movement that caused it.

For this example, we assume that Alice deposits $1,000.00 by ACH along with 5 ACME shares, and that Bob deposits 50 ZETA shares. Note that the amount issued for USD is 100000, because the fundamental unit of the USD asset is a cent. We can do all three of these actions in a single transaction:

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("usd")
    .setAmount(100000)
    .setDestinationAccountId("alice")
    .addActionTagsField("type", "deposit")
    .addActionTagsField("system", "ach")
    .addActionTagsField("ach_transaction_id", "11111")
  ).addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("acme")
    .setAmount(5)
    .setDestinationAccountId("alice")
    .addActionTagsField("type", "deposit")
  ).addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("zeta")
    .setAmount(50)
    .setDestinationAccountId("bob")
    .addActionTagsField("type", "deposit")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetId: 'usd',
    amount: 100000,
    destinationAccountId: 'alice',
    actionTags: {
      type: 'deposit',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  })
  builder.issue({
    assetId: 'acme',
    amount: 5,
    destinationAccountId: 'alice',
    actionTags: {
      type: 'deposit'
    }
  })
  builder.issue({
    assetId: 'zeta',
    amount: 50,
    destinationAccountId: 'bob',
    actionTags: {
      type: 'deposit'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_id: 'usd',
    amount: 100000,
    destination_account_id: 'alice',
    action_tags: {
      type: 'deposit',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  )
  builder.issue(
    asset_id: 'acme',
    amount: 5,
    destination_account_id: 'alice',
    action_tags: {
      type: 'deposit'
    }
  )
  builder.issue(
    asset_id: 'zeta',
    amount: 50,
    destination_account_id: 'bob',
    action_tags: {
      type: 'deposit'
    }
  )
end

Because this transaction issues tokens of the USD asset, it must be signed by the treasury key. This is handled automatically by the transact SDK method.

Settle a Trade Against CashLink to this section

When one investor buys a security from another, we model the settlement as an atomic transaction with two actions:

  1. Transfer - the purchase price from the buyer to the seller
  2. Transfer - the security from the seller to the buyer

This is delivery versus payment, and atomicity is the whole point of doing it in one transaction: the ledger either records both legs or neither, so there is no state in which the cash has moved and the security has not.

In this example, we assume that Alice buys 1 ZETA share from Bob for $500.00.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("usd")
    .setAmount(50000)
    .setSourceAccountId("alice")
    .setDestinationAccountId("bob")
    .addActionTagsField("type", "dvp")
    .addActionTagsField("tx_id", "1234")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("zeta")
    .setAmount(1)
    .setSourceAccountId("bob")
    .setDestinationAccountId("alice")
    .addActionTagsField("type", "dvp")
    .addActionTagsField("tx_id", "1234")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.transfer({
    assetId: 'usd',
    amount: 50000,
    sourceAccountId: 'alice',
    destinationAccountId: 'bob',
    actionTags: {
      type: 'dvp',
      tx_id: '1234'
    }
  })
  builder.transfer({
    assetId: 'zeta',
    amount: 1,
    sourceAccountId: 'bob',
    destinationAccountId: 'alice',
    actionTags: {
      type: 'dvp',
      tx_id: '1234'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.transfer(
    asset_id: 'usd',
    amount: 50000,
    source_account_id: 'alice',
    destination_account_id: 'bob',
    action_tags: {
      type: 'dvp',
      tx_id: '1234'
    }
  )
  builder.transfer(
    asset_id: 'zeta',
    amount: 1,
    source_account_id: 'bob',
    destination_account_id: 'alice',
    action_tags: {
      type: 'dvp',
      tx_id: '1234'
    }
  )
end

Settle a Security-for-Security ExchangeLink to this section

When two investors exchange one security for another, the transaction is similar to the previous one, except that both legs are securities.

  1. Transfer - the first security from the first investor to the second
  2. Transfer - the second security from the second investor to the first

The ratio is agreed by the parties, and priced by the platform, before the transaction is submitted. The ledger records the exchange; it does not decide whether the terms were fair.

In this example, we assume that Alice transfers 1 ACME share to Bob in exchange for 15 ZETA shares.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("acme")
    .setAmount(1)
    .setSourceAccountId("alice")
    .setDestinationAccountId("bob")
    .addActionTagsField("type", "security_swap")
    .addActionTagsField("tx_id", "5678")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("zeta")
    .setAmount(15)
    .setSourceAccountId("bob")
    .setDestinationAccountId("alice")
    .addActionTagsField("type", "security_swap")
    .addActionTagsField("tx_id", "5678")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.transfer({
    assetId: 'acme',
    amount: 1,
    sourceAccountId: 'alice',
    destinationAccountId: 'bob',
    actionTags: {
      type: 'security_swap',
      tx_id: '5678'
    }
  })
  builder.transfer({
    assetId: 'zeta',
    amount: 15,
    sourceAccountId: 'bob',
    destinationAccountId: 'alice',
    actionTags: {
      type: 'security_swap',
      tx_id: '5678'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.transfer(
    asset_id: 'acme',
    amount: 1,
    source_account_id: 'alice',
    destination_account_id: 'bob',
    action_tags: {
      type: 'security_swap',
      tx_id: '5678'
    }
  )
  builder.transfer(
    asset_id: 'zeta',
    amount: 15,
    source_account_id: 'bob',
    destination_account_id: 'alice',
    action_tags: {
      type: 'security_swap',
      tx_id: '5678'
    }
  )
end

Sale Outside the PlatformLink to this section

If an investor sells a security to a counterparty who is not a participant, there is nothing on this ledger to transfer it to. Instead we retire the security being sold, because it passes out of the platform's custody, and issue what the investor receives in return, because that arrives from outside.

  1. Retire - the security being sold, from the investor's account
  2. Issue - the cash or securities received, into that investor's account

We can use action tags to record details about the transaction, such as a trade reference or information about the incoming wire.

For this example, assume Bob sells 1 ACME share to an external purchaser for $9,000.00.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Retire()
    .setAssetId("acme")
    .setAmount(1)
    .setSourceAccountId("bob")
    .addActionTagsField("type", "external_sale")
  ).addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("usd")
    .setAmount(900000)
    .setDestinationAccountId("bob")
    .addActionTagsField("type", "external_sale")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.retire({
    assetId: 'acme',
    amount: 1,
    sourceAccountId: 'bob',
    actionTags: {
      type: 'external_sale'
    }
  })
  builder.issue({
    assetId: 'usd',
    amount: 900000,
    destinationAccountId: 'bob',
    actionTags: {
      type: 'external_sale'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.retire(
    asset_id: 'acme',
    amount: 1,
    source_account_id: 'bob',
    action_tags: {
      type: 'external_sale'
    }
  )
  builder.issue(
    asset_id: 'usd',
    amount: 900000,
    destination_account_id: 'bob',
    action_tags: {
      type: 'external_sale'
    }
  )
end

WithdrawLink to this section

When an investor withdraws cash, the operator takes a 1% fee and remits the remainder. We model this as a single atomic transaction with two actions:

  1. Retire - the fee amount, from the investor's account
  2. Retire - the remaining amount, from the investor's account

We can use action tags to record details about the withdrawal, such as the payment method and the associated transaction ID in that external system. Note that we use two retire actions rather than one for the full amount, so that fees can be queried separately afterwards. See the Queries section for an example.

For this example, we'll assume that Alice withdraws $200.00 by ACH.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Retire()
    .setAssetId("usd")
    .setAmount(200)
    .setSourceAccountId("alice")
    .addActionTagsField("type", "withdrawal_fee")
  ).addAction(new Transaction.Builder.Action.Retire()
    .setAssetId("usd")
    .setAmount(19800)
    .setSourceAccountId("alice")
    .addActionTagsField("type", "withdrawal")
    .addActionTagsField("system", "ACH")
    .addActionTagsField("ach_transaction_id", "22222")
    ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.retire({
    assetId: 'usd',
    amount: 200,
    sourceAccountId: 'alice',
    actionTags: {
      type: 'withdrawal_fee'
    }
  })
  builder.retire({
    assetId: 'usd',
    amount: 19800,
    sourceAccountId: 'alice',
    actionTags: {
      type: 'withdrawal',
      system: 'ach',
      ach_transaction_id: '22222'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.retire(
    asset_id: 'usd',
    amount: 200,
    source_account_id: 'alice',
    action_tags: {
      type: 'withdrawal_fee'
    }
  )
  builder.retire(
    asset_id: 'usd',
    amount: 19800,
    source_account_id: 'alice',
    action_tags: {
      type: 'withdrawal',
      system: 'ach',
      ach_transaction_id: '22222'
    }
  )
end

Since this transaction retires from an investor's account, it must be signed by the exchange key. This is handled automatically by the transact SDK method.

Note that instead of retiring the fee, we could have transferred it to an operator account. That design, however, would leave those tokens sitting in the operator's account 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 withdrawal_fee.

QueriesLink to this section

Now that we have created several transactions, we can query the ledger in various ways.

Investor HoldingsLink to this section

If we want to know what every account holds, we perform a sum tokens query with no filter (so we count every token) and group the results by account id and asset id.

TokenSum.ItemIterable balances = new Token.SumBuilder()
  .addGroupByField("accountId")
  .addGroupByField("assetId")
  .getIterable(ledger);

for (TokenSum balance : balances) {
  System.out.println("account: " + balance.accountId);
  System.out.println("amount: " + balance.amount );
  System.out.println("asset: " + balance.assetId);
  System.out.println("");
}
var page1 = ledger.tokens.sum({
  groupBy: ['accountId', 'assetId']
}).page()

page1.items.forEach(balance => {
  console.log('account: ' + balance.accountId)
  console.log('amount: ' + balance.amount)
  console.log('asset: ' + balance.assetId)
  console.log('')
})
ledger.tokens.sum(
  group_by: ['account_id', 'asset_id']
).each do |balance|
  puts 'account: ' + balance.account_id
  puts 'amount: ' + balance.amount.to_s
  puts 'asset: ' + balance.asset_id
  puts ''
end

which will output:

account: alice
amount: 16
asset: zeta

(etc.)

Securities on the PlatformLink to this section

If we want to know how much of each security the platform holds across all accounts, we perform a sum tokens query, filtering to the security type in asset tags and grouping the results by asset id.

TokenSum.ItemIterable balances = new Token.SumBuilder()
  .setFilter("assetTags.type=$1")
  .addFilterParameter("security")
  .addGroupByField("assetId")
  .getIterable(ledger);

for (TokenSum balance : balances) {
  System.out.println("amount: " + balance.amount);
  System.out.println("asset: " + balance.assetId);
  System.out.println("");
}
page1 = ledger.tokens.sum({
  filter: 'assetTags.type=$1',
  filterParams: ['security'],
  groupBy: ['assetId']
}).page()

page1.items.forEach(balance => {
  console.log('amount: ' + balance.amount)
  console.log('asset: ' + balance.assetId)
  console.log('')
})
ledger.tokens.sum(
  filter: 'asset_tags.type=$1',
  filter_params: ['security'],
  group_by: ['asset_id']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts 'asset: ' + balance.asset_id
  puts ''
end

which will output:

amount: 4
asset: acme

amount: 50
asset: zeta

Total 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 withdrawal_fee type in action tags.

ActionSum.ItemIterable sums = new Action.SumBuilder()
  .setFilter("tags.type=$1")
  .addFilterParameter("withdrawal_fee")
  .getIterable(ledger);

for (ActionSum sum : sums) {
  System.out.println("total fees: " + sum.amount);
}
page1 = ledger.actions.sum({
  filter: 'tags.type=$1',
  filterParams: ['withdrawal_fee']
}).page()

page1.items.forEach(sum => {
  console.log('total fees: ' + sum.amount)
})
ledger.actions.sum(
  filter: 'tags.type=$1',
  filter_params: ['withdrawal_fee']
).each do |sum|
  puts 'total fees: ' + sum.amount.to_s
end

Investor ActivityLink to this section

If we want to retrieve historical transactions, we use the list transactions query. We can specify the number of transactions we want to retrieve at a time by setting a page size. If we only care about transactions on a specific account, we can filter to those that have an action with that account as the source or destination by using actions() in a filter.

The below will return the 10 most recent transactions that involved Alice's account.

Transaction.Page txs = new Transaction.ListBuilder()
  .setFilter("actions(sourceAccountId=$1 OR destinationAccountId=$1)")
  .addFilterParameter("alice")
  .setPageSize(10)
  .getPage(ledger);
page1 = ledger.transactions.list({
  filter: 'actions(sourceAccountId=$1 OR destinationAccountId=$1)',
  filterParams: ['alice']
}).page({size: 10})
page1 = ledger.transactions.list(
  filter: 'actions(source_account_id=$1 OR destination_account_id=$1)',
  filter_params: ['alice']
).page(size: 10)

If we only cared about the specific actions involving the account (as opposed to the entire transaction), we could use the list actions query instead. This will return Action objects, whereas the above query will return Transaction objects.

Action.Page txs = new Action.ListBuilder()
  .setFilter("sourceAccountId=$1 OR destinationAccountId=$1")
  .addFilterParameter("alice")
  .setPageSize(10)
  .getPage(ledger);
page1 = ledger.actions.list({
  filter: 'sourceAccountId=$1 OR destinationAccountId=$1',
  filterParams: ['alice']
}).page({size: 10})
page1 = ledger.actions.list(
  filter: 'source_account_id=$1 OR destination_account_id=$1',
  filter_params: ['alice']
).page(size: 10)