Building a ride-sharing application

Two-sided marketplace applications — such as ride-sharing or vacation rental apps — are easy to build on top of Sequence. These applications tend to have a few things in common — buyers pay providers, the company takes a cut, and the providers get paid out periodically.

In this guide, we explore how to build a ride-sharing application on top of Sequence.

Overview

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

Additionally, we will create a processing account and a company account. Fares will be transferred into the processing account and then divided between the driver and company accounts based on the fare share rate.

For each each country / region in which the marketplace operates, we will represent fares, refunds, and promotional credits independently, as assets in the ledger. Each one of these will be denominated in the appropriate currency. So, for example, if there were two different regions with distinct currencies, we would have two fare assets, one for each currency.

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 two distinct systems:

  1. Operations - responsible for processing ride payments, issuing refunds to users, paying out drivers, and collecting the company portion of the fares
  2. Promotions - responsible for issuing promotional credit to users

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

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

new Key.Builder()
  .setAlias("promotions")
  .create(ledger);
ledger.keys.create({alias: 'operations'})
ledger.keys.create({alias: 'promotions'})
ledger.keys.create(alias: 'operations')
ledger.keys.create(alias: 'promotions')

Assets

We will need three types of assets for each currency:

  • fare - used to track earnings by drivers and the company
  • refund - used to track refunds distributed to riders as credit for future rides
  • promotion - used to track promotional credit distributed to riders for future rides

We will use tags to denominate the currency of each asset for aggregate queries.

For this example, we will assume operations only in the Unites States, and therefore will only need assets demonimated in USD. If we had more than one currency, we would create the above for each one.

First we create the fare and refund USD assets using the operations key:

new Asset.Builder()
  .setAlias("fare_usd")
  .addKeyByAlias("operations")
  .addTag("currency", "usd")
  .addTag("type", "fare")
  .create(ledger);

new Asset.Builder()
  .setAlias("refund_usd")
  .addKeyByAlias("operations")
  .addTag("currency", "usd")
  .addTag("type", "fare")
  .create(ledger);
ledger.assets.create({
  alias: 'fare_usd',
  keys: [{alias: 'operations'}],
  tags: {
    currency: 'usd',
    type: 'fare'
  }
})

ledger.assets.create({
  alias: 'refund_usd',
  keys: [{alias: 'operations'}],
  tags: {
    currency: 'usd',
    type: 'refund'
  }
})
ledger.assets.create(
  alias: 'fare_usd',
  keys: [{alias: 'operations'}],
  tags: {
    currency: 'usd',
    type: 'fare'
  }
)

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

Next, we create the promotion USD asset using the promotions key:

new Asset.Builder()
  .setAlias("promotion_usd")
  .addKeyByAlias("promotions")
  .addTag("currency", "usd")
  .addTag("type", "promotion")
  .create(ledger);
ledger.assets.create({
  alias: 'promotion_usd',
  keys: [{alias: 'promotions'}],
  tags: {
    currency: 'usd',
    type: 'promotion'
  }
})
ledger.assets.create(
  alias: 'promotion_usd',
  keys: [{alias: 'promotions'}],
  tags: {
    currency: 'usd',
    type: 'promotion'
  }
)

Accounts

For each rider and driver, we will need an account in the ledger. Although these accounts would actually be created by the ride-sharing application in real-time, for this example we'll assume we have one rider and one driver, and we'll create them as part of the setup.

We also need the process account, which will process payments, and the company account, which will track the company's portion of the fares.

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

We create the company, processing, driver, and rider accounts using the operations key.

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

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

new Account.Builder()
  .setAlias("driver1")
  .addKeyByAlias("operations")
  .addTag("type", "driver")
  .create(ledger);

new Account.Builder()
  .setAlias("rider1")
  .addKeyByAlias("operations")
  .addTag("type", "rider")
  .create(ledger);
ledger.accounts.create({
  alias: 'company',
  keys: [{alias: 'operations'}],
  tags: {type: 'company'}
})

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

ledger.accounts.create({
  alias: 'driver1',
  keys: [{alias: 'operations'}],
  tags: {type: 'driver'}
})

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

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

ledger.accounts.create(
  alias: 'driver1',
  keys: [{alias: 'operations'}],
  tags: {type: 'driver'}
)

ledger.accounts.create(
  alias: 'rider1',
  keys: [{alias: 'operations'}],
  tags: {type: 'rider'}
)

Transaction Types

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

Ride Payment

When a rider pays a ride with credit card, we create an atomic transaction containing four actions:

  • issue - the fare amount of the fare asset into rider's account to represent the credit card charge
  • transfer - the fare amount of the fare asset from rider's account to processing account to represent the fare payment
  • transfer - the driver's portion of the fare asset from the processing account to the driver's account
  • transfer - the company's portion of the fare asset from the processing account to the company's account

We use action reference data to record details about each action for later queries.

For this example, we assume that Rider1 pays a $20.00 fare for a ride with Driver1, and the company takes 10% of the fare ($2.00). Note that the amount of issuance is 2000, because the fundamental unit of the fare USD asset is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("fare_usd")
    .setAmount(2000)
    .setDestinationAccountAlias("rider1")
    .addReferenceDataField("type", "credit_charge")
    .addReferenceDataField("credit_charge_id", "123")
    .addReferenceDataField("rider_profile", "personal")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("fare_usd")
    .setAmount(2000)
    .setSourceAccountAlias("rider1")
    .setDestinationAccountAlias("processing")
    .addReferenceDataField("type", "fare_payment")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("fare_usd")
    .setAmount(1800)
    .setSourceAccountAlias("processing")
    .setDestinationAccountAlias("driver1")
    .addReferenceDataField("type", "driver_fare_share")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("fare_usd")
    .setAmount(200)
    .setSourceAccountAlias("processing")
    .setDestinationAccountAlias("company")
    .addReferenceDataField("type", "company_fare_share")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'fare_usd',
    amount: 2000,
    destinationAccountAlias: 'rider1',
    referenceData: {
      type: 'credit_charge',
      credit_charge_id: '123',
      rider_profile: 'personal'
    }
  })
  builder.transfer({
    assetAlias: 'fare_usd',
    amount: 2000,
    sourceAccountAlias: 'rider1',
    destinationAccountAlias: 'processing',
    referenceData: {type: 'fare_payment'}
  })
  builder.transfer({
    assetAlias: 'fare_usd',
    amount: 1800,
    sourceAccountAlias: 'processing',
    destinationAccountAlias: 'driver1',
    referenceData: {type: 'driver_fare_share'}
  })
  builder.transfer({
    assetAlias: 'fare_usd',
    amount: 200,
    sourceAccountAlias: 'processing',
    destinationAccountAlias: 'company',
    referenceData: {type: 'company_fare_share'}
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'fare_usd',
    amount: 2000,
    destination_account_alias: 'rider1',
    reference_data: {
      type: 'credit_charge',
      credit_charge_id: '123',
      rider_profile: 'personal'
    }
  )
  builder.transfer(
    asset_alias: 'fare_usd',
    amount: 2000,
    source_account_alias: 'rider1',
    destination_account_alias: 'processing',
    reference_data: {type: 'fare_payment'}
  )
  builder.transfer(
    asset_alias: 'fare_usd',
    amount: 1800,
    source_account_alias: 'processing',
    destination_account_alias: 'driver1',
    reference_data: {type: 'driver_fare_share'}
  )
  builder.transfer(
    asset_alias: 'fare_usd',
    amount: 200,
    source_account_alias: 'processing',
    destination_account_alias: 'company',
    reference_data: {type: 'company_fare_share'}
  )
end

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

Refund Rider

When the company decides to refund a rider, we create a trasaction with a single action, issuing the refund USD asset into the rider's account.

We can use action reference data to record the reason for the refund.

For this example, we assume that Rider1 is refunded $5.00 because the driver took a poor route, but the company covers the cost with no impact to the driver's portion of the previously collected fare.

Note that the amount of issuance is 500, because the fundamental unit of the refund USD asset is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("refund_usd")
    .setAmount(500)
    .setDestinationAccountAlias("rider1")
    .addReferenceDataField("type", "refund")
    .addReferenceDataField("reason", "poor_route")
    .addReferenceDataField("ride_id", "123")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'refund_usd',
    amount: 500,
    destinationAccountAlias: 'rider1',
    referenceData: {
      type: 'refund',
      reason: 'poor_route',
      ride_id: '123'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'refund_usd',
    amount: 500,
    destination_account_alias: 'rider1',
    reference_data: {
      type: 'refund',
      reason: 'poor_route',
      ride_id: '123'
    }
  )
end

Since this transaction issues the refund USD asset, it must be signed by the operations key. This is handled automatically by the transact SDK method.

Ride Payment - with credits

When a rider has credits (either refund or promotional) available in their account, they can choose to apply them to a fare. We model this as a single atomic transaction with five actions:

  • retire - payment amount of credits from rider's account to be exchanged for the fare asset
  • issue - the fare amount of the fare asset into rider's account to represent the exchange of credits
  • transfer - the fare amount of the fare asset from rider's account to the processing account to represent the fare payment
  • transfer - the driver's portion of the fare asset from the processing account to the driver's account
  • transfer - the company's portion of the fare asset from the processing account to the company's account

For this example, we assume that Rider1 pays a $5.00 fare for a ride with Driver1 with refund USD in their account. The company takes 10% of the fare ($0.50).

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Retire()
    .setAssetAlias("refund_usd")
    .setAmount(500)
    .setSourceAccountAlias("rider1")
    .addReferenceDataField("type", "redeem_credits")
  ).addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("fare_usd")
    .setAmount(500)
    .setDestinationAccountAlias("rider1")
    .addReferenceDataField("type", "redeem_credits")
    .addReferenceDataField("rider_profile", "personal")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("fare_usd")
    .setAmount(500)
    .setSourceAccountAlias("rider1")
    .setDestinationAccountAlias("processing")
    .addReferenceDataField("type", "fare_payment")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("fare_usd")
    .setAmount(450)
    .setSourceAccountAlias("processing")
    .setDestinationAccountAlias("driver1")
    .addReferenceDataField("type", "driver_fare_share")
  ).addAction(new Transaction.Builder.Action.Transfer()
    .setAssetAlias("fare_usd")
    .setAmount(50)
    .setSourceAccountAlias("processing")
    .setDestinationAccountAlias("company")
    .addReferenceDataField("type", "company_fare_share")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.retire({
    assetAlias: 'refund_usd',
    amount: 500,
    sourceAccountAlias: 'rider1',
    referenceData: {type: 'redeem_credits'}
  })
  builder.issue({
    assetAlias: 'fare_usd',
    amount: 500,
    destinationAccountAlias: 'rider1',
    referenceData: {type: 'redeem_credits'}
  })
  builder.transfer({
    assetAlias: 'fare_usd',
    amount: 500,
    sourceAccountAlias: 'rider1',
    destinationAccountAlias: 'processing',
    referenceData: {type: 'fare_payment'}
  })
  builder.transfer({
    assetAlias: 'fare_usd',
    amount: 450,
    sourceAccountAlias: 'processing',
    destinationAccountAlias: 'driver1',
    referenceData: {type: 'driver_fare_share'}
  })
  builder.transfer({
    assetAlias: 'fare_usd',
    amount: 50,
    sourceAccountAlias: 'processing',
    destinationAccountAlias: 'company',
    referenceData: {type: 'company_fare_share'}
  })
})
ledger.transactions.transact do |builder|
  builder.retire(
    asset_alias: 'refund_usd',
    amount: 500,
    source_account_alias: 'rider1',
    reference_data: {type: 'redeem_credits'}
  )
  builder.issue(
    asset_alias: 'fare_usd',
    amount: 500,
    destination_account_alias: 'rider1',
    reference_data: {type: 'redeem_credits'}
  )
  builder.transfer(
    asset_alias: 'fare_usd',
    amount: 500,
    source_account_alias: 'rider1',
    destination_account_alias: 'processing',
    reference_data: {type: 'fare_payment'}
  )
  builder.transfer(
    asset_alias: 'fare_usd',
    amount: 450,
    source_account_alias: 'processing',
    destination_account_alias: 'driver1',
    reference_data: {type: 'driver_fare_share'}
  )
  builder.transfer(
    asset_alias: 'fare_usd',
    amount: 50,
    source_account_alias: 'processing',
    destination_account_alias: 'company',
    reference_data: {type: 'company_fare_share'}
  )
end

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

Distribute Promotional Credits

When the company decides to issue promotional credits to a rider, we create a transaction with a single action, issuing the promotional USD asset into the rider's account.

We can use action reference data to record details about the promotion.

For this example, we assume that Rider1 is distributed $3.00 because they referred a friend.

Note that the amount of issuance is 300, because the fundamental unit of the promotional USD asset is a cent.

new Transaction.Builder()
  .addAction(new Transaction.Builder.Action.Issue()
    .setAssetAlias("promotion_usd")
    .setAmount(300)
    .setDestinationAccountAlias("rider1")
    .addReferenceDataField("type", "promotion")
    .addReferenceDataField("campaign", "referral")
  ).transact(ledger);
ledger.transactions.transact(builder => {
  builder.issue({
    assetAlias: 'promotion_usd',
    amount: 300,
    destinationAccountAlias: 'rider1',
    referenceData: {
      type: 'promotion',
      campaign: 'referral'
    }
  })
})
ledger.transactions.transact do |builder|
  builder.issue(
    asset_alias: 'promotion_usd',
    amount: 300,
    destination_account_alias: 'rider1',
    reference_data: {
      type: 'promotion',
      campaign: 'referral'
    }
  )
end

Since this transaction issues the promotion USD asset, it must be signed by the promotions key. This is handled automatically by the transact SDK method.

Payout Driver

When the company pays out a driver balance, we create a transaction with a single action retiring the balance of the fare asset from the driver'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 the company pays Driver1 the balance of $20 via ACH. Note that the amount being retired is 2000, because the fundamental unit of the fare USD asset is a cent.

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

Since this transaction retires from a driver account, it must be signed by the operations 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.

Driver Balance

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

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

Balance.ItemIterable balances = new Balance.QueryBuilder()
  .setFilter("account_alias=$1 AND asset_alias=$2")
  .addFilterParameter("driver1")
  .addFilterParameter("fare_usd")
  .getIterable(ledger);
for (Balance balance : balances) {
  System.out.println("amount: " + balance.amount);
  System.out.println("");
}
ledger.balances.queryAll({
  filter: 'account_alias=$1 AND asset_alias=$2',
  filterParams: ['driver1', 'fare_usd']
}).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_alias=$2',
  filter_params: ['driver1', 'fare_usd']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts ''
end

Which will output:

amount: x

This is the amount that would be ready for driver payout.

Driver earnings by ride

If we want to know the amount that the driver earned for their rides, we create a transaction query, filtering to ones that include actions with reference_data.type='driver_fare_share' and the account_alias is the driver's account.

Transaction.ItemIterable txs = new Transaction.QueryBuilder()
  .setFilter("actions(reference_data.type=$1 AND destination_account_alias=$2)")
  .addFilterParameter("driver_fare_share")
  .addFilterParameter("driver1")
  .getIterable(ledger);
for (Transaction tx : txs) {
  int fare = 0;
  int driver_share = 0;
  for (Transaction.Action action : tx.actions) {
    if ("fare_payment".equals(action.referenceData.get("type"))) {
      System.out.println("fare: " + action.amount);
    }
    if ("driver_fare_share".equals(action.referenceData.get("type"))) {
      System.out.println("driver_share: " + action.amount);
      System.out.println("");
    }
  }
}
ledger.transactions.queryAll({
  filter: 'actions(reference_data.type=$1 AND destination_account_alias=$2)',
  filterParams: ['driver_fare_share', 'driver1']
}).then(txs => {
  for (let i in txs) {
    const tx = txs[i]
    var fare = 0
    var driver_share = 0
    for (let j in tx.actions) {
      const action = tx.actions[j]
      if (action.referenceData.type == 'fare_payment') {fare = action.amount}
      if (action.referenceData.type == 'driver_fare_share') {driver_share = action.amount}
    }
    console.log("fare: " + fare)
    console.log("driver_share: " + driver_share)
    console.log("")
  }
})
ledger.transactions.query(
  filter: 'actions(reference_data.type=$1 AND destination_account_alias=$2)',
  filter_params: ['driver_fare_share', 'driver1']
).each do |balance|
  fare = 0
  driver_share = 0
  balance.actions.each do |action|
    if action['reference_data']['type'] == 'fare_payment'
      fare = action.amount
    elsif action['reference_data']['type'] == 'driver_fare_share'
      driver_share = action.amount
    end
  end
  puts 'fare: ' + fare.to_s
  puts 'driver_share: ' + driver_share.to_s
  puts ''
end

Which will output:

fare: x
driver_share: y

fare: z
driver_share: a

...

Rider Credits Balance

If we want to know the amount of credits a rider has available to spend on rides, we need to perform a balance query summing the refund and promotions assets in the rider's account.

We accomplish this by filtering to the rider's account_alias and assets where asset_tags.type= 'usd'. We then sum the results by asset_tags.type to aggregate the two types of credits - which both have asset_tags.type = 'usd'.

Balance.ItemIterable balances = new Balance.QueryBuilder()
  .setFilter("account_alias=$1 AND asset_tags.currency=$2")
  .addFilterParameter("rider1")
  .addFilterParameter("usd")
  .addSumByField("asset_tags.type")
  .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.currency=$2',
  filterParams: ['rider1','usd'],
  sumBy: ['asset_tags.type']
}).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.currency=$2',
  filter_params: ['rider1','usd'],
  sum_by: ['asset_tags.type']
).each do |balance|
  puts 'amount: ' + balance.amount.to_s
  puts ''
end

Which will output:

amount: x

Rider payments by ride

If we want to know the amount that a rider paid for each of their rides, we create a transaction query, filtering to ones that include actions with reference_data.type='fare_payment' and source_account_alias='rider1'.

Transaction.ItemIterable txs = new Transaction.QueryBuilder()
  .setFilter("actions(reference_data.type=$1 AND source_account_alias=$2)")
  .addFilterParameter("fare_payment")
  .addFilterParameter("rider1")
  .getIterable(ledger);
for (Transaction tx : txs) {
  for (Transaction.Action action : tx.actions) {
    if ("fare_payment".equals(action.referenceData.get("type"))) {
      System.out.println("fare: " + action.amount);
      System.out.println("");
    }
  }
}
ledger.transactions.queryAll({
  filter: 'actions(reference_data.type=$1 AND source_account_alias=$2)',
  filterParams: ['fare_payment', 'rider1']
}).then(txs => {
  for (let i in txs) {
    const tx = txs[i]
    for (let j in tx.actions) {
      const action = tx.actions[j]
      if (action.referenceData.type == 'fare_payment') {
        console.log("fare: " + action.amount)
        console.log("")
      }
    }
  }
})
ledger.transactions.query(
  filter: 'actions(reference_data.type=$1 AND source_account_alias=$2)',
  filter_params: ['fare_payment', 'rider1']
).each do |balance|
  balance.actions.each do |action|
    if action['reference_data']['type'] == 'fare_payment'
      puts 'fare: ' + action.amount.to_s
      puts ''
    end
  end
end

Which will output:

fare: x

fare: y

...