Building an Uber Eats Clone
Templating money flows with Numscript
We explore in this article the money problem behind creating a food delivery platform, focusing on building the core money movements using the Numary ledger and Numscript as the foundation of our real-time accounting system.
The Three-sided Platform
From a money flow perspective, Eats is a classic n-sided marketplace (where N=3). It is a machine that reads your credit card details, materializes a pizza on your porch and a platform, facilitates a transaction between three parties: the customer, the restaurant and the rider.
Some payments services providers give us constructs to route an incoming payment, to a certain extent. Stripe Connect for example has destination charges, which give you the ability to provide the end destination account and your commission amount at payment. This is not super helpful for our food delivery app given that:
- We have more than 1 destination (the restaurant and rider)
- We don't know, at payment time, who is going to be the rider
To these issues, Stripe answers with another turnkey solution called separate charges and transfers. It basically solves all our concerns, at one cost: we are now responsible for tracking what is owed to whom. Ledger systems are a good candidate for achieving this, as we'll attempt to demonstrate in this article.
Templating our flow
Not bothering with unique values (for now), let's start to encode our money flow in Numscript using hardcoded values. If you want to follow along, Numscript files can be executed against a locally running numary ledger directly from the cli:
numary exec "eats-clone-demo" example.num
Using an intermediate payment ledger account helps us divide our flow into multiple sub-problems. Prior to splitting, our first mission is to properly track how much we collected from our customer. Let's start with a simple case by assuming we managed to collect $59 through Stripe:
We use here the
@world account to introduce money into our ledger (as per the spec). If its USD balance was zero before this transaction, it will now be negative 59. We can note as well that we use here
USD/2 instead of
USD, which is basically a notation for USD scaled to 10^-2 i.e. USD cents.
We conclude the payment by flushing the funds we collected to an order account, which will receive the funds without ever bothering about their origin:
Philosophically, we consider stripe as an external ledger that now owes money to our local ledger. In the real world, money hasn't moved but we penciled in that this money is now dedicated to this order.
Moving further in the flow, we now task ourselves with splitting the customer payment to the rider, restaurant and our platform. This splitting script is typically where we would encode our platform commission logic. We make the decision here of taking a 15% cut on each amount we pay the restaurant, leaving the delivery fee entirely to the rider.
We now have riders and and restaurants accounts in our ledger with a positive balance, whose purpose is to eventually be paid out to their owners' banks.On the rider and restaurant views of our app, we'll typically display these balances as "available for payout" funds.
Sticking to Stripe, one way to achive the processing of these balances can be to move the balance from a
restaurants:001 account to a
restaurants:001:payouts:001 account, initiating a transfer and a payout on Stripe of that amount, and eventually moving the balance from
restaurants:001:payouts:001 back to
world once we successfully processed the payout.
Automating our flow
Using hardcoded values in Numscript will only get us so far. As we start to execute our templates automatically from a backend, we will typically start to use variables, passing them along with the script on the /script endpoint
Using execution variables
Using a commission from metadata
As we iterate with our platform economics, we'll likely end up with different commission rates per restaurant. We can inject this commission as a variable as well, fetching it the example below from the restaurant account metadata:
Inundating the buy side with generous coupons is a common strategy to break the chicken and egg problem platforms face. If you're really going for an Eats clone, you may subsidize the buy side so much that you'll end up spending more for a purchase in discount than the commission you'll get out of it:
In the grand scheme of the above example, as a platform, we are $8 short overall. If this was the only payment flow to ever happen on our Stripe account, we wouldn't be able to cover payouts to the rider and restaurant.
As we rinse and repeat this process for thousands of sales, it becomes critical to properly track these marketing losses to avoid putting ourselves at risk of ever defaulting payouts.
Numscript comes with multi-sources transactions which we can use to model our marketing expense. Let's first credit $15k to a coupon account for a specific campaign:
Then in script executing at payment time, we'll use this coupons as a source of marketing funds, from which we'll draw up to $19.
Refunds at loss
So, the food arrived colder than it left the freezer at the restaurant. Customer files a ticket and you can't know for sure wether the weather, rider, or restaurant is at fault here. As a platform you may decide to eat the loss, issuing a partial refund to your customer.
Properly registering this loss as a money movement in our ledger will, in addition to keeping the books in check, once again help us prevent defaulting risk as we'll be able to create routine processes of topping-up your Stripe platform account to cover for losses.
The internal flow we defined in blue sits between input and output. It tracks what is owed to whom, and only makes sense if it eventually supports actual money movements in the real world, which we achieve through payments processors.
Recon is a complex payments 101 problem. We want to make sure that assumptions we make in our system, e.g. that we received $59 from our customer, hold true in the external systems we use over time. As an example, your customer may charge back their payment after a few weeks, breaking your reconciliation. Ideally, every payment on your provider should be linked to an internal ledger transaction. In a further article, we'll cover techniques and scripts to continuously reconcile our flows.
Notes on legal design and compliance
We mentioned defaulting risk a few times in this piece. I've heard objections to this concern, usually quoting perpetual growth combined with positive cash float as a mechanism to make up for platform losses and marketing expenses. This can only be true if properly measured with a ledgering strategy and to the surprise of no one, auditors are not particularily fond of this scheme, no matter how exciting the idea of a fractional-reserve marketplace sounds on paper.
In any case, your local lawyers are the best advisors on what you can and cannot do with your payments flows.
As a platform, having a holistic view of your money flows and being able to know at any point in time what is owed to whom is critical. Using a ledger-oriented programmation model will help us achieve both reliability and visibility, as immutable transactions become the fundamental construct of state in our system.