Examples

Building a Marketplace Settlement Network

A platform that stands between buyers and suppliers collects from one side, keeps a margin, and pays the other side later. Between collection and payout it is holding money that belongs to somebody else, and it has to be able to show, per supplier and per invoice, exactly how much and since when.

On this page

In this guide, we model that: buyers settling invoices, the operator taking its fee, suppliers accruing a balance and being paid out, and early-settlement credits the operator funds itself.

OverviewLink to this section

There are two types of users: buyers and suppliers. These will each be represented as accounts in the ledger.

We will also create a processing account. Every settlement passes through it and is divided there between the supplier and the operator, so the money in transit is visible as a balance rather than implied by the difference between two other numbers.

For each currency the marketplace settles in, we will represent settlements, refunds, and credits as tokens in the ledger, with an asset for each type of balance. Each is denominated in a single currency, so two currencies means two sets of assets.

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. Operations - responsible for processing settlements, issuing refunds, paying out suppliers, and collecting the operator's fee
  2. Credits - responsible for issuing early-settlement credits

Each system will have a key that will be used to perform its actions in the ledger. To create these keys, we run the following:

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

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

AssetsLink to this section

For each currency supported by the platform, the ledger will need two assets. For this example, we will assume the platform only supports USD. Therefore, the two assets will be:

  • usd - represents amounts deposited or earned
  • credit_usd - credits issued to buyers, redeemable against future invoices

We will use tags to record details like the denomination currency and the type of balance, to make querying easier later.

We will create usd and credit_usd with the operations key and credits key, respectively:

new Asset.Builder()
  .setId("usd")
  .addKeyId("operations")
  .addTag("currency", "usd")
  .addTag("type", "cash")
  .create(ledger);

new Asset.Builder()
  .setId("credit_usd")
  .addKeyId("credits")
  .addTag("currency", "usd")
  .addTag("type", "credit")
  .create(ledger);
ledger.assets.create({
  id: 'usd',
  keyIds: ['operations'],
  tags: {
    currency: 'usd',
    type: 'cash'
  }
})

ledger.assets.create({
  id: 'credit_usd',
  keyIds: ['credits'],
  tags: {
    currency: 'usd',
    type: 'credit'
  }
})
ledger.assets.create(
  id: 'usd',
  key_ids: ['operations'],
  tags: {
    currency: 'usd',
    type: 'cash'
  }
)

ledger.assets.create(
  id: 'credit_usd',
  key_ids: ['credits'],
  tags: {
    currency: 'usd',
    type: 'credit'
  }
)

AccountsLink to this section

Each buyer and supplier will need an account in the ledger. Although these accounts would be created as participants are onboarded, for this example we'll assume we have one of each, and we'll create their accounts as part of the setup.

We also need a processing account, which will process payments.

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

new Account.Builder()
  .setId("supplier1")
  .addKeyId("operations")
  .addTag("type", "supplier")
  .create(ledger);

new Account.Builder()
  .setId("buyer1")
  .addKeyId("operations")
  .addTag("type", "buyer")
  .create(ledger);

  new Account.Builder()
    .setId("processing")
    .addKeyId("operations")
    .addTag("type", "processing")
    .create(ledger);
ledger.accounts.create({
  id: 'supplier1',
  keyIds: ['operations'],
  tags: {type: 'supplier'}
})

ledger.accounts.create({
  id: 'buyer1',
  keyIds: ['operations'],
  tags: {type: 'buyer'}
})

ledger.accounts.create({
  id: 'processing',
  keyIds: ['operations'],
  tags: {type: 'processing'}
})
ledger.accounts.create(
  id: 'supplier1',
  key_ids: ['operations'],
  tags: {type: 'supplier'}
)

ledger.accounts.create(
  id: 'buyer1',
  key_ids: ['operations'],
  tags: {type: 'buyer'}
)

ledger.accounts.create(
  id: 'processing',
  key_ids: ['operations'],
  tags: {type: 'processing'}
)

Transaction TypesLink to this section

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

Settle an InvoiceLink to this section

When a buyer settles an invoice by card, we create an atomic transaction containing four actions:

  1. Issue - usd tokens equal to the invoice total into the buyer's account, representing the card charge
  2. Transfer - the same amount of usd tokens from the buyer's account to the processing account
  3. Transfer - an amount of the usd tokens equal to the supplier's portion from the processing account to the supplier's account
  4. Retire - the remaining usd tokens from the processing account, which is the operator's fee

We use action tags to record details about each action and distinguish between the different types.

For this example, assume that Buyer1 settles a $20.00 invoice from Supplier1, and the operator takes 10% ($2.00). Note that the amount issued is 2000, because the fundamental unit of the USD asset is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("usd")
    .setAmount(2000)
    .setDestinationAccountId("buyer1")
    .addActionTagsField("type", "buyer_funding")
    .addActionTagsField("funding_id", "123")
    .addActionTagsField("buyer_profile", "standard")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("usd")
    .setAmount(2000)
    .setSourceAccountId("buyer1")
    .setDestinationAccountId("processing")
    .addActionTagsField("type", "invoice_payment")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("usd")
    .setAmount(1800)
    .setSourceAccountId("processing")
    .setDestinationAccountId("supplier1")
    .addActionTagsField("type", "supplier_share")
  ).addAction(new Transaction.Builder.Action.Retire()
    .setAssetId("usd")
    .setAmount(200)
    .setSourceAccountId("processing")
    .addActionTagsField("type", "operator_fee")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetId: 'usd',
    amount: 2000,
    destinationAccountId: 'buyer1',
    actionTags: {
      type: 'buyer_funding',
      funding_id: '123',
      buyer_profile: 'standard'
    }
  })
  builder.transfer({
    assetId: 'usd',
    amount: 2000,
    sourceAccountId: 'buyer1',
    destinationAccountId: 'processing',
    actionTags: {type: 'invoice_payment'}
  })
  builder.transfer({
    assetId: 'usd',
    amount: 1800,
    sourceAccountId: 'processing',
    destinationAccountId: 'supplier1',
    actionTags: {type: 'supplier_share'}
  })
  builder.retire({
    assetId: 'usd',
    amount: 200,
    sourceAccountId: 'processing',
    actionTags: {type: 'operator_fee'}
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_id: 'usd',
    amount: 2000,
    destination_account_id: 'buyer1',
    action_tags: {
      type: 'buyer_funding',
      funding_id: '123',
      buyer_profile: 'standard'
    }
  )
  builder.transfer(
    asset_id: 'usd',
    amount: 2000,
    source_account_id: 'buyer1',
    destination_account_id: 'processing',
    action_tags: {type: 'invoice_payment'}
  )
  builder.transfer(
    asset_id: 'usd',
    amount: 1800,
    source_account_id: 'processing',
    destination_account_id: 'supplier1',
    action_tags: {type: 'supplier_share'}
  )
  builder.retire(
    asset_id: 'usd',
    amount: 200,
    source_account_id: 'processing',
    action_tags: {type: 'operator_fee'}
  )
end

Since this transaction issues usd and transfers between accounts, it must be signed by the operations key. This is handled automatically by the transact SDK method.

Refund BuyerLink to this section

When the operator refunds a buyer, we create a transaction with a single action, issuing usd tokens into the buyer's account.

We can use action tags to record the reason for the refund.

Assume that Buyer1 is refunded $5.00 for a late delivery, and the operator absorbs the cost, leaving the supplier's share of the original settlement untouched.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("usd")
    .setAmount(500)
    .setDestinationAccountId("buyer1")
    .addActionTagsField("type", "refund")
    .addActionTagsField("reason", "late_delivery")
    .addActionTagsField("order_id", "123")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetId: 'usd',
    amount: 500,
    destinationAccountId: 'buyer1',
    actionTags: {
      type: 'refund',
      reason: 'late_delivery',
      order_id: '123'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_id: 'usd',
    amount: 500,
    destination_account_id: 'buyer1',
    action_tags: {
      type: 'refund',
      reason: 'late_delivery',
      order_id: '123'
    }
  )
end

Because this transaction issues usd, it must be signed by the operations key. This is handled automatically by the transact SDK method.

Issue CreditsLink to this section

When the operator grants a buyer credit against future invoices, we create a transaction with a single action, issuing the credit asset into the buyer's account.

Assume that Buyer1 is granted $3.00 of credit under an early-settlement programme.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("credit_usd")
    .setAmount(300)
    .setDestinationAccountId("buyer1")
    .addActionTagsField("type", "credit")
    .addActionTagsField("campaign", "referral")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetId: 'credit_usd',
    amount: 300,
    destinationAccountId: 'buyer1',
    actionTags: {
      type: 'credit',
      campaign: 'referral'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_id: 'credit_usd',
    amount: 300,
    destination_account_id: 'buyer1',
    action_tags: {
      type: 'credit',
      campaign: 'referral'
    }
  )
end

Because this transaction issues credit_usd, it must be signed by the credits key. This is handled automatically by the transact SDK method.

Settle an Invoice - with creditsLink to this section

When a buyer holds credits, they can apply them to an invoice. We model this as a single atomic transaction with five actions:

  1. Retire - the amount of credits that will be used from the buyer's account
  2. Issue - the same amount of usd into the buyer's account
  3. Transfer - the full invoice amount in usd from the buyer's account to the processing account
  4. Transfer - an amount of usd equal to the supplier's portion from the processing account to the supplier's account
  5. Retire - usd tokens equal to the operator's fee from the processing account

Note that we exchange credit_usd for usd first, so that the settlement itself is always a single usd transfer for the full invoice amount however it was funded. That keeps the later query for settlement history simple. See the Queries section below.

Assume that Buyer1 settles a $5.00 invoice from Supplier1, using $3.00 of credits. The operator's fee is 10% ($0.50).

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Retire()
    .setAssetId("credit_usd")
    .setAmount(300)
    .setSourceAccountId("buyer1")
    .addActionTagsField("type", "redeem_credits")
  ).addAction(new Transaction.Builder.Action.Issue()
    .setAssetId("usd")
    .setAmount(300)
    .setDestinationAccountId("buyer1")
    .addActionTagsField("type", "cash_value_of_credits")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("usd")
    .setAmount(500)
    .setSourceAccountId("buyer1")
    .setDestinationAccountId("processing")
    .addActionTagsField("type", "invoice_payment")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetId("usd")
    .setAmount(450)
    .setSourceAccountId("processing")
    .setDestinationAccountId("supplier1")
    .addActionTagsField("type", "supplier_share")
  ).addAction(new Transaction.Builder.Action.Retire()
    .setAssetId("usd")
    .setAmount(50)
    .setSourceAccountId("processing")
    .addActionTagsField("type", "operator_fee")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.retire({
    assetId: 'credit_usd',
    amount: 300,
    sourceAccountId: 'buyer1',
    actionTags: {type: 'redeem_credits'}
  })
  builder.issue({
    assetId: 'usd',
    amount: 300,
    destinationAccountId: 'buyer1',
    actionTags: {type: 'cash_value_of_credits'}
  })
  builder.transfer({
    assetId: 'usd',
    amount: 500,
    sourceAccountId: 'buyer1',
    destinationAccountId: 'processing',
    actionTags: {type: 'invoice_payment'}
  })
  builder.transfer({
    assetId: 'usd',
    amount: 450,
    sourceAccountId: 'processing',
    destinationAccountId: 'supplier1',
    actionTags: {type: 'supplier_share'}
  })
  builder.retire({
    assetId: 'usd',
    amount: 50,
    sourceAccountId: 'processing',
    actionTags: {type: 'operator_fee'}
  })
})
ledger.transactions.transact do |builder|
  builder.retire(
    asset_id: 'credit_usd',
    amount: 300,
    source_account_id: 'buyer1',
    action_tags: {type: 'redeem_credits'}
  )
  builder.issue(
    asset_id: 'usd',
    amount: 300,
    destination_account_id: 'buyer1',
    action_tags: {type: 'cash_value_of_credits'}
  )
  builder.transfer(
    asset_id: 'usd',
    amount: 500,
    source_account_id: 'buyer1',
    destination_account_id: 'processing',
    action_tags: {type: 'invoice_payment'}
  )
  builder.transfer(
    asset_id: 'usd',
    amount: 450,
    source_account_id: 'processing',
    destination_account_id: 'supplier1',
    action_tags: {type: 'supplier_share'}
  )
  builder.retire(
    asset_id: 'usd',
    amount: 50,
    source_account_id: 'processing',
    action_tags: {type: 'operator_fee'}
  )
end

Payout SupplierLink to this section

When the operator pays out a supplier's balance, we create a transaction with a single action retiring usd from the supplier's account in the amount paid out. The money leaves the network, so the tokens standing for it are retired.

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

Assume that the operator pays Supplier1 $20.00 by ACH.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Retire()
    .setAssetId("usd")
    .setAmount(2000)
    .setSourceAccountId("supplier1")
    .addActionTagsField("type", "supplier_payout")
    .addActionTagsField("system", "ach")
    .addActionTagsField("ach_transaction_id", "11111")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.retire({
    assetId: 'usd',
    amount: 2000,
    sourceAccountId: 'supplier1',
    actionTags: {
      type: 'supplier_payout',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.retire(
    asset_id: 'usd',
    amount: 2000,
    source_account_id: 'supplier1',
    action_tags: {
      type: 'supplier_payout',
      system: 'ach',
      ach_transaction_id: '11111'
    }
  )
end

QueriesLink to this section

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

Supplier BalanceLink to this section

If we want to know the balance that a supplier has earned but has not yet been paid out, we perform a sum tokens query, filtering to the supplier's account id and the USD asset.

For example, let's query the balance in Supplier1's account.

TokenSum.ItemIterable balances = new Token.SumBuilder()
  .setFilter("accountId=$1 AND assetId=$2")
  .addFilterParameter("supplier1")
  .addFilterParameter("usd")
  .getIterable(ledger);

for (TokenSum balance : balances) {
  System.out.println("amount: " + balance.amount);
  System.out.println("");
}
var page1 = ledger.tokens.sum({
  filter: 'accountId=$1 AND assetId=$2',
  filterParams: ['supplier1', 'usd']
}).page()

page1.items.forEach(balance => {
  console.log('amount: ' + balance.amount)
  console.log('')
})
ledger.tokens.sum(
  filter: 'account_id=$1 AND asset_id=$2',
  filter_params: ['supplier1', 'usd']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts ''
end

which will output:

amount: x

Supplier Earning HistoryLink to this section

If we want to know what a supplier has earned, settlement by settlement, we create a list actions query, filtering to actions with type supplier_share and a destination of that supplier's account. Setting the page size to 10 returns their ten most recent.

Action.Page actions = new Action.ListBuilder()
  .setFilter("tags.type=$1 AND destinationAccountId=$2")
  .addFilterParameter("supplier_share")
  .addFilterParameter("supplier1")
  .setPageSize(10)
  .getPage(ledger);

for (Action action : actions.items) {
  System.out.println("amount: " + action.amount);
  System.out.println("");
}
page1 = ledger.actions.list({
  filter: 'tags.type=$1 AND destinationAccountId=$2',
  filterParams: ['supplier_share', 'supplier1']
}).page({size: 10})

page1.items.forEach(action => {
  console.log('amount: ' + action.amount)
  console.log('')
})
page1 = ledger.actions.list(
  filter: 'tags.type=$1 AND destination_account_id=$2',
  filter_params: ['supplier_share', 'supplier1']
).page(size: 10)

page1.each do |action|
  puts 'amount: ' + action.amount.to_s
  puts ''
end

which will output:

amount: x

amount: y

(etc.)

Buyer Credit BalancesLink to this section

If we want to know how much credit each buyer has left, we perform a sum tokens query filtering to the credit asset and to accounts tagged 'buyer', grouping by account id.

TokenSum.ItemIterable balances = new Token.SumBuilder()
  .setFilter("assetId=$1 AND accountTags.type=$2")
  .addFilterParameter("credit_usd")
  .addFilterParameter("buyer")
  .addGroupByField("accountId")
  .getIterable(ledger);

for (TokenSum balance : balances) {
  System.out.println("account: " + balance.accountId);
  System.out.println("amount: " + balance.amount);
  System.out.println("");
}
page1 = ledger.tokens.sum({
  filter: 'assetId=$1 AND accountTags.type=$2',
  filterParams: ['credit_usd','buyer'],
  groupBy: ['accountId']
}).page()

page1.items.forEach(balance => {
  console.log('account: ' + balance.accountId)
  console.log('amount: ' + balance.amount)
  console.log('')
})
ledger.tokens.sum(
  filter: 'asset_id=$1 AND account_tags.type=$2',
  filter_params: ['credit_usd','buyer'],
  group_by: ['account_id']
).each do |balance|
  puts 'account: ' + balance.account_id
  puts 'amount: ' + balance.amount.to_s
  puts ''
end

which will output:

account: ...
amount: ...

(etc.)

Buyer Settlement HistoryLink to this section

If we want to know what a buyer paid on each of their recent invoices, we create a list actions query, filtering to actions with a type of invoice_payment and a source account of 'buyer1'.

Action.Page actions = new Action.ListBuilder()
  .setFilter("tags.type=$1 AND sourceAccountId=$2")
  .addFilterParameter("invoice_payment")
  .addFilterParameter("buyer1")
  .setPageSize(10)
  .getPage(ledger);

for (Action action : actions.items) {
  System.out.println("amount: " + action.amount);
  System.out.println("");
}
page1 = ledger.actions.list({
  filter: 'tags.type=$1 AND sourceAccountId=$2',
  filterParams: ['invoice_payment', 'buyer1']
}).page({size: 10})

page1.items.forEach(action => {
  console.log('amount: ' + action.amount)
  console.log('')
})
page1 = ledger.actions.list(
  filter: 'tags.type=$1 AND source_account_id=$2',
  filter_params: ['invoice_payment', 'buyer1']
).page(size: 10)

page1.each do |action|
  puts 'amount: ' + action.amount.to_s
  puts ''
end

which will output:

amount: x

amount: y

...