Building a lending platform

Lending platforms are easy to build on top of Sequence. These applications need to track how much borrowers owe, as well as facilitate the packaging and sale of loans to investors.

In this guide, we explore how to build a platform for making installment loans to individual borrowers and selling them to investors on top of Sequence.

Overview

There are two types of users: borrowers and investors. Each investor will be represented in the ledger by an account. The company will have three accounts: one for processing borrower payments, one for tracking loans owned by the company, and one for collecting revenue received in connection with selling and servicing loans.

Each loan will be represented by its own unique asset, which will be tagged with details about the borrower. Additionally, there will be an asset for each currency that the platform supports. In this example, we will assume operations only in the Unites States, and therefore will only need a USD asset.

Setup

To set up our ledger, we will create several keys, assets, and accounts.

Keys

Authority to create transactions in the ledger is assigned to three distinct systems:

  1. Underwriting - responsible for originating loans
  2. Processing - responsible for processing payments by borrowers
  3. Sales - responsible for bundling and selling loans to investors

Each system will have a key that will be used to perform its actions in the ledger. Let's create a key for each system:

new Key.Builder()
  .setAlias("underwriting")
  .create(ledger);

new Key.Builder()
  .setAlias("processing")
  .create(ledger);

new Key.Builder()
  .setAlias("sales")
  .create(ledger);
ledger.keys.create({alias: 'underwriting'})
ledger.keys.create({alias: 'processing'})
ledger.keys.create({alias: 'sales'})
ledger.keys.create(alias: 'underwriting')
ledger.keys.create(alias: 'processing')
ledger.keys.create(alias: 'sales')

Assets

We will need a loan asset for each loan originated. Although these assets would actually be created by the platform in real-time, for this example, we'll assume we have two loans and create them as part of the setup. We will use tags to track details on the loan, such as borrower information, loan type, and economic terms. We also need an asset for each currency being used, to represent cash.

For this example, we will assume there are only two loans, with loan_id of 1 and 2. In loan 1, the borrower is receiving $100 and committing to pay that money back plus $20 in interest, in monthly payments spread across the life of the loan. In loan 2, the borrower is receiving $200 and committing to pay that money back plus $40 in interest, in monthly payments spread across the life of the loan. Note that the principal amounts recorded in the tags are 10000 and 20000, respectively, because the fundamental unit of the loan assets is a cent. All amounts in tags are recorded the same way.

We will assume that loan 1 and 2 are both made to the same borrower (with borrower_id of 123). We will also assume the only currency used is USD. If we had more than one currency, we would create an asset for each one.

We create the loan assets using the underwriting key and the USD asset with the processing key:

new Asset.Builder()
  .setAlias("loan1")
  .addKeyByAlias("underwriting")
  .addTag("type", "loan")
  .addTag("loan_id", "1")
  .addTag("borrower_id", "123")
  .addTag("currency", "usd")
  .addTag("principal", "10000")
  .addTag("interest_rate", "0.1")
  .addTag("total_interest", "1000")
  .addTag("issue_date", "09/01/2017")
  .addTag("schedule", "monthly")
  .addTag("maturity_date", "03/01/2018")
  .create(ledger);

new Asset.Builder()
  .setAlias("loan2")
  .addKeyByAlias("underwriting")
  .addTag("type", "loan")
  .addTag("loan_id", "2")
  .addTag("borrower_id", "123")
  .addTag("currency", "usd")
  .addTag("principal", "20000")
  .addTag("interest_rate", "0.2")
  .addTag("total_interest", "4000")
  .addTag("issue_date", "02/01/2018")
  .addTag("schedule", "monthly")
  .addTag("maturity_date", "05/01/2018")
  .create(ledger);

new Asset.Builder()
  .setAlias("usd")
  .addKeyByAlias("processing")
  .addTag("type", "currency")
  .create(ledger);
ledger.assets.create({
  alias: 'loan1',
  keys: [{alias: 'underwriting'}],
  tags: {
    type: 'loan',
    loan_id: '1',
    borrower_id: '123',
    currency: 'usd',
    principal: '10000',
    interest_rate: '0.1',
    total_interest: '1000',
    issue_date: '09/01/2017',
    schedule: 'monthly',
    maturity_date: '03/01/2018'
  }
})

ledger.assets.create({
  alias: 'loan2',
  keys: [{alias: 'underwriting'}],
  tags: {
    type: 'loan',
    loan_id: '2',
    borrower_id: '123',
    currency: 'usd',
    principal: '20000',
    interest_rate: '0.2',
    total_interest: '4000',
    issue_date: '02/01/2017',
    maturity_date: '05/01/2018'
  }
})

ledger.assets.create({
  alias: 'usd',
  keys: [{alias: 'processing'}],
  tags: {
    type: 'currency'
  }
})
ledger.assets.create(
  alias: 'loan1',
  keys: [{alias: 'underwriting'}],
  tags: {
    type: 'loan',
    loan_id: '1',
    borrower_id: '123',
    currency: 'usd',
    principal: '10000',
    interest_rate: '0.1',
    total_interest: '1000',
    issue_date: '09/01/2017',
    schedule: 'monthly',
    maturity_date: '03/01/2018'
  }
)

ledger.assets.create(
  alias: 'loan2',
  keys: [{alias: 'underwriting'}],
  tags: {
    type: 'loan',
    loan_id: '2',
    borrower_id: '123',
    currency: 'usd',
    principal: '20000',
    interest_rate: '0.2',
    total_interest: '4000',
    issue_date: '02/01/2017',
    maturity_date: '05/01/2018'
  }
)

ledger.assets.create(
  alias: 'usd',
  keys: [{alias: 'processing'}],
  tags: {
    type: 'currency'
  }
)

Accounts

For each investor that joins the platform, we will need an account in the ledger. Although these accounts would actually be created by the platform in real-time, for this example we'll assume we have one investor and create it as part of the setup. We also need the company lending, processing, and revenue accounts.

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

We create the investor and processing accounts using the processing key, and the lending and revenue accounts using the sales key:

new Account.Builder()
  .setAlias("investor1")
  .addKeyByAlias("processing")
  .addTag("type", "investor")
  .create(ledger);

new Account.Builder()
  .setAlias("processing")
  .addKeyByAlias("processing")
  .addTag("type", "company")
  .create(ledger);

new Account.Builder()
  .setAlias("lending")
  .addKeyByAlias("sales")
  .addTag("type", "company")
  .create(ledger);

new Account.Builder()
  .setAlias("revenue")
  .addKeyByAlias("sales")
  .addTag("type", "company")
  .create(ledger);
ledger.accounts.create({
  alias: 'investor1',
  keys: [{alias: 'processing'}],
  tags: {type: 'investor'}
})

ledger.accounts.create({
  alias: 'processing',
  keys: [{alias: 'processing'}],
  tags: {type: 'company'}
})

ledger.accounts.create({
  alias: 'lending',
  keys: [{alias: 'sales'}],
  tags: {type: 'company'}
})

ledger.accounts.create({
  alias: 'revenue',
  keys: [{alias: 'sales'}],
  tags: {type: 'company'}
})
ledger.accounts.create(
  alias: 'investor1',
  keys: [{alias: 'processing'}],
  tags: {type: 'investor'}
)

ledger.accounts.create(
  alias: 'processing',
  keys: [{alias: 'processing'}],
  tags: {type: 'company'}
)

ledger.accounts.create(
  alias: 'lending',
  keys: [{alias: 'sales'}],
  tags: {type: 'company'}
)

ledger.accounts.create(
  alias: 'revenue',
  keys: [{alias: 'sales'}],
  tags: {type: 'company'}
)

Transaction Types

Now that we have created our assets and accounts, we can model the different types of transactions.

Issue Loan

When the company makes a loan, we create a transaction containing an issue action to issue the amount of the loan asset into the company lending account. For our example, we are assuming these are simple installment loans, in which the entire amount to be paid is determined up front and paid back over a fixed period of time. Therefore, the amount of the asset issued is the total that will be paid by the borrower over the life of the loan (including any interest).

We can use action reference data to record details about the loan origination, such as the payment system used to fund the loan.

For this example, we assume that the total amount owed on loan1 is $110 and on loan2 is $240. Note that the amounts of issuance are 11000 and 24000, respectively, because the fundamental unit of both loan assets is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("loan1")
    .setAmount(11000)
    .setDestinationAccountAlias("lending")
    .addReferenceDataField("type", "issue_loan")
    .addReferenceDataField("system", "wire")
    .addReferenceDataField("wire_transaction_id", "11111")
  ).transact(ledger);

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("loan2")
    .setAmount(24000)
    .setDestinationAccountAlias("lending")
    .addReferenceDataField("type", "issue_loan")
    .addReferenceDataField("system", "ACH")
    .addReferenceDataField("ach_transaction_id", "22222")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'loan1',
    amount: 11000,
    destinationAccountAlias: 'lending',
    referenceData: {
      type: 'issue_loan',
      system: 'wire',
      wire_transaction_id: '11111'
    }
  })
})

ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'loan2',
    amount: 24000,
    destinationAccountAlias: 'lending',
    referenceData: {
      type: 'issue_loan',
      system: 'ACH',
      ach_transaction_id: '22222'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'loan1',
    amount: 11000,
    destination_account_alias: 'lending',
    reference_data: {
      type: 'issue_loan',
      system: 'wire',
      wire_transaction_id: '11111'
    }
  )
end

ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'loan2',
    amount: 24000,
    destination_account_alias: 'lending',
    reference_data: {
      type: 'issue_loan',
      system: 'ACH',
      ach_transaction_id: '22222'
    }
  )
end

Since these transactions issue loan assets, they must be signed by the underwriting key. This is handled automatically by the transact SDK method.

Investor Deposit

Investors can deposit cash into the platform for the purpose of purchasing loans. We model this as an issue action into the investor's account.

  • issue - USD equal to the deposit

Suppose investor1 deposits $1000 onto the platform.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("usd")
    .setAmount(100000)
    .setDestinationAccountAlias("investor1")
    .addReferenceDataField("type", "deposit")
    .addReferenceDataField("system", "wire")
    .addReferenceDataField("wire_transaction_id", "22222")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'usd',
    amount: 100000,
    destinationAccountAlias: 'investor1',
    referenceData: {
      type: 'deposit',
      system: 'wire',
      wire_transaction_id: '22222'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'usd',
    amount: 100000,
    destination_account_alias: 'investor1',
    reference_data: {
      type: 'deposit',
      system: 'wire',
      wire_transaction_id: '22222'
    }
  )
end

Sell Loans

When the company sells one or more loans to an investor, the investor pays the company an upfront cash amount and the company transfers ownership of the loan(s) to the investor. The company also collects processing fees on borrower payments (which is shown in the next section).

We model this as a transaction followed by an update of the tags. The initial transaction is made up of multiple actions:

  • transfer - USD equal to the purchase price from investor's account to the company revenue account
  • transfer - for each loan being purchased, the loan assets for the purchased loans from the company lending account to the investor account

For this example, we will assume investor1 purchases loan1 and loan2 for $340 and the sale occurs before the borrower has made their first monthly payment. Note that the amount of USD is 34000 because the fundamental unit of the USD asset is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("usd")
    .setAmount(34000)
    .setSourceAccountAlias("investor1")
    .setDestinationAccountAlias("revenue")
    .addReferenceDataField("type", "loan_purchase")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("loan1")
    .setAmount(11000)
    .setSourceAccountAlias("lending")
    .setDestinationAccountAlias("investor1")
    .addReferenceDataField("type", "loan_purchase")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("loan2")
    .setAmount(24000)
    .setSourceAccountAlias("lending")
    .setDestinationAccountAlias("investor1")
    .addReferenceDataField("type", "loan_purchase")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.transfer({
    assetAlias: 'usd',
    amount: 34000,
    sourceAccountAlias: 'investor1',
    destinationAccountAlias: 'revenue',
    referenceData: {type: 'loan_purchase'}
  })
  builder.transfer({
    assetAlias: 'loan1',
    amount: 11000,
    sourceAccountAlias: 'lending',
    destinationAccountAlias: 'investor1',
    referenceData: {type: 'loan_purchase'}
  })
  builder.transfer({
    assetAlias: 'loan2',
    amount: 24000,
    sourceAccountAlias: 'lending',
    destinationAccountAlias: 'investor1',
    referenceData: {type: 'loan_purchase'}
  })
})
ledger.transactions.transact do |builder|
  builder.transfer(
    asset_alias: 'usd',
    amount: 34000,
    source_account_alias: 'investor1',
    destination_account_alias: 'revenue',
    reference_data: {type: 'loan_purchase'}
  )
  builder.transfer(
    asset_alias: 'loan1',
    amount: 11000,
    source_account_alias: 'lending',
    destination_account_alias: 'investor1',
    reference_data: {type: 'loan_purchase'}
  )
  builder.transfer(
    asset_alias: 'loan2',
    amount: 24000,
    source_account_alias: 'lending',
    destination_account_alias: 'investor1',
    reference_data: {type: 'loan_purchase'}
  )
end

Borrower Payment

Borrower payments flow first into the processing account, and then to the account of the loan owner (i.e., the company lending account or an investor account). If an investor is the owner, a portion of the payment is transferred to the company revenue account as a processing fee. We model this as a single atomic transaction with four actions.

  1. Issue - USD equal to the payment to the processing account
  2. Transfer - USD equal to the processing fee from the processing account to the company revenue account (if applicable)
  3. Transfer - USD equal to the payment minus the processing fee from the processing account to the investor account
  4. Retire - the loan asset equal to the full payment (including processing fee) from the investor account

If the company is the owner of the loan, we skip step 2.

Suppose the borrower makes a payment of $10 on loan1, which has been sold to investor1, and the company collects a 0.5% processing fee. Note that the amount of loan1 retired is 1000 because the fundamental unit of the asset is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("usd")
    .setAmount(1000)
    .setDestinationAccountAlias("processing")
    .addReferenceDataField("type", "loan_payment")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("usd")
    .setAmount(5)
    .setSourceAccountAlias("processing")
    .setDestinationAccountAlias("revenue")
    .addReferenceDataField("type", "processing_fee")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("usd")
    .setAmount(995)
    .setSourceAccountAlias("processing")
    .setDestinationAccountAlias("investor1")
    .addReferenceDataField("type", "serviced_loan_payment")
  ).addAction(new Transaction.Builder.Action.Retire()
    .setAssetAlias("loan1")
    .setAmount(1000)
    .setSourceAccountAlias("investor1")
    .addReferenceDataField("type", "loan_payment")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'usd',
    amount: 1000,
    destinationAccountAlias: 'processing',
    referenceData: {
      type: 'loan_payment'
    }
  })
  builder.transfer({
    assetAlias: 'usd',
    amount: 5,
    sourceAccountAlias: 'processing',
    destinationAccountAlias: 'revenue',
    referenceData: {
      type: 'processing_fee'
    }
  })
  builder.transfer({
    assetAlias: 'usd',
    amount: 995,
    sourceAccountAlias: 'processing',
    destinationAccountAlias: 'investor1',
    referenceData: {
      type: 'serviced_loan_payment'
    }
  })
  builder.retire({
    assetAlias: 'loan1',
    amount: 1000,
    sourceAccountAlias: 'investor1',
    referenceData: {
      type: 'loan_payment'
    }
  })
})
tx = ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'usd',
    amount: 1000,
    destination_account_alias: 'processing',
    reference_data: {
      type: 'loan_payment'
    }
  )
  builder.transfer(
    asset_alias: 'usd',
    amount: 5,
    source_account_alias: 'processing',
    destination_account_alias: 'revenue',
    reference_data: {
      type: 'processing_fee'
    }
  )
  builder.transfer(
    asset_alias: 'usd',
    amount: 995,
    source_account_alias: 'processing',
    destination_account_alias: 'investor1',
    reference_data: {
      type: 'serviced_loan_payment'
    }
  )
  builder.retire(
    asset_alias: 'loan1',
    amount: 1000,
    source_account_alias: 'investor1',
    reference_data: {
      type: 'loan_payment'
    }
  )
end

Because this transaction transfers and retires from the company processing account and an investor account, it must be signed by the processing key. This is handled automatically by the transact SDK method.

Investor Withdrawal

When an investor withdraws cash from their account on the platform, we create a transaction with a single action retiring an amount of the USD asset equal to the withdrawal amount from the investor's account.

We can use action reference data to record details about the withdrawal, such as the withdrawal method and associated transaction ID in that external system.

For this example, we'll assume that investor1 withdraws $50 from their account via ACH. Note that the amount being retired is 5000, because the fundamental unit of the fare USD asset is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Retire()
    .setAssetAlias("usd")
    .setAmount(5000)
    .setSourceAccountAlias("investor1")
    .addReferenceDataField("type", "investor_withdrawal")
    .addReferenceDataField("system", "ach")
    .addReferenceDataField("ach_transaction_id", "11111")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.retire({
    assetAlias: 'usd',
    amount: 5000,
    sourceAccountAlias: 'investor1',
    referenceData: {
      type: 'investor_withdrawal',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  })
})
tx = ledger.transactions.transact do |builder|
  builder.retire(
    asset_alias: 'usd',
    amount: 5000,
    source_account_alias: 'investor1',
    reference_data: {
      type: 'investor_withdrawal',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  )
end

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

Queries

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

Investor Portfolio

If we want to know all of the loans an investor currenly owns, we perform an asset query, filtering to the investor account_alias and the loan asset type.

Balance.ItemIterable balances = new Balance.QueryBuilder()
  .setFilter("account_alias=$1 AND asset_tags.type=$2")
  .addFilterParameter("investor1")
  .addFilterParameter("loan")
  .addSumByField("asset_alias")
  .getIterable(ledger);
for (Balance balance : balances) {
  System.out.println("amount: " + balance.amount );
  System.out.println("loan: " + balance.sumBy.get("asset_alias"));
  System.out.println("");
}
ledger.balances.queryAll({
  filter: 'account_alias=$1 AND asset_tags.type=$2',
  filterParams: ['investor1', 'loan'],
  sumBy: ['asset_alias']
}).then(balances => {
  for (let i in balances) {
    const balance = balances[i];
    console.log('amount: ' + balance.amount )
    console.log('loan: ' + balance.sumBy.assetAlias)
    console.log('')
  }
})
ledger.balances.query(
  filter: 'account_alias=$1 AND asset_tags.type=$2',
  filter_params: ['investor1', 'loan'],
  sum_by: ['asset_alias']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts 'loan: ' + balance.sum_by['asset_alias'].to_s
  puts ''
end

Which will output:

amount: x
loan: y

amount: w
loan: u

Borrower Loans

If we want to know all the amounts that a particular borrower has outstanding, we perform a balance query and sum the results by asset_alias.

Balance.ItemIterable balances = new Balance.QueryBuilder()
  .setFilter("asset_tags.borrower_id=$1")
  .addFilterParameter("123")
  .addSumByField("asset_alias")
  .getIterable(ledger);
for (Balance balance : balances) {
  System.out.println("amount: " + balance.amount );
  System.out.println("loan: " + balance.sumBy.get("asset_alias"));
  System.out.println("");
}
ledger.balances.queryAll({
  filter: 'asset_tags.borrower_id=$1',
  filterParams: ['123'],
  sumBy: ['asset_alias']
}).then(balances => {
  for (let i in balances) {
    const balance = balances[i];
    console.log('amount: ' + balance.amount )
    console.log('loan: ' + balance.sumBy.assetAlias )
    console.log('')
  }
})
ledger.balances.query(
  filter: 'asset_tags.borrower_id=$1',
  filter_params: ['123'],
  sum_by: ['asset_alias']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts 'loan: ' + balance.sum_by['asset_alias']
  puts ''
end

Which will output:

amount: x
loan: usd

amount: y
loan: points

Investor Cash Balance

If we want to know the amount of each asset in an account, we perform a balance query, filtering to the account alias and assets with type currency, summing the results by asset alias.

For example, let's list the cash balance in investor1's account.

Balance.ItemIterable balances = new Balance.QueryBuilder()
  .setFilter("account_alias=$1 AND asset_tags.type=$2")
  .addFilterParameter("investor1")
  .addFilterParameter("currency")
  .addSumByField("asset_alias")
  .getIterable(ledger);
for (Balance balance : balances) {
  System.out.println("amount: " + balance.amount );
  System.out.println("");
}
ledger.balances.queryAll({
  filter: 'account_alias=$1 AND asset_tags.type=$2',
  filterParams: ['investor1', 'currency'],
  sumBy: ['asset_alias']
}).then(balances => {
  for (let i in balances) {
    const balance = balances[i];
    console.log('amount: ' + balance.amount )
    console.log('')
  }
})
ledger.balances.query(
  filter: 'account_alias=$1 AND asset_tags.type=$2',
  filter_params: ['investor1', 'currency'],
  sum_by: ['asset_alias']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts ''
end

Which will output:

amount: x

amount: y