September 20, 2022
Product
5
 min read

How we built seamless rewards redemption

Harry Ford
Harry Ford
Senior Backend Engineer

Tap and go

We built Yonder’s rewards program with one non-negotiable principle; that redeeming a reward should be remarkably easy. Just a bit better than our competitors wasn’t enough, we had to be miles ahead.

That means no coupons, no codes, no ‘activating’ rewards and no painful web experiences. Yonder rewards had to be the most intuitive and seamless credit card rewards program in the world. Delivering on that principle is really, really hard, which is why no one has done it before. We had our work cut out for us.

On top of that, we had really high expectations for how our internal teams should be able to manage and deliver our complex series of rewards benefits, called Local Experiences, to our customers. Our team needed to be able to create rich content, deliver that across our apps and content channels, and be able to make changes to a Local Experience at any time.

Here I’ll break down how we got to where we are as accurately as I can, relaying our thinking at the time, and how we learnt from each iteration of Local Experiences to arrive at the way we do it now.

Matching experiences

Every month, Yonder curate a handful of dining experiences from around London and let our members use their points for free meals or drinks (up to a fair use limit) or earn extra points just for dining there.

We had a big vision that just tapping your Yonder card at a Local Experience partner would be enough to redeem your points, and that meant being able to identify a partner instantly when a transaction was made.

V1 – Merchant Description Matching

We knew we'd have to match data coming through from our card network by looking to uniquely identify our experience merchants. Unfortunately, there's no silver bullet magic ID that tells you exactly which merchant a transaction has been made at and the location of the merchant (if only).

When you make a transaction, Mastercard send us a load of information about that transaction and within that there were some options for us identify a merchant partner. We started off with an exact match of the merchant description and its postcode to match the transaction data to what we had saved, and we were confident it must be a genuine match. However, some experience partners had multiple locations, and with a small team unable to test every location of every experience, we only realised that merchants tend to be quite liberal with the information they send back once we had more users to try out all the branches for us. We found that we were not redeeming every experience because sometimes the merchant would send a very slightly different description for their Soho or King's Cross restaurant, and sometimes even different terminals within the same branch of a restaurant would send back different data.

V2 – Adding MCC and the Levenshtein distance

We quickly added an MCC (merchant category code) to the mix and started using the Levenshtein distance of the raw description from the card network, as well as clean description we got from our transaction enrichment partners compared to the description we were expecting from the merchant. The Levenshtein distance calculates how close a string is to another string, so Crust Bros and Crust Brothers would record a high score where Crust Bros and Pizza hut would not (the Hut won't be featuring as an experience anytime soon). To calculate the right number for the Levenshtein we used test driven development with various test cases of real merchant data that had come through. When we were expecting a match we looked at the numbers that were coming through compared to our expected data.

Here’s a test:

This approach meant we tended to catch when merchants had different data at different terminals and we even had an extra piece of data we could match on to ensure it was, in fact, an experience the user was visiting. There was only one problem with this: we no longer required a postcode to match. This meant we occasionally got matches with MCC and a close description that weren't actual experiences. MCCs are significantly less unique than postcodes; that is, most restaurants send back an MCC of 5812 or 5813. We were therefore occasionally catching restaurants that had similar names to our experiences, but weren't actual experiences that we offered, as they matched on the MCC and description.

When we got false positives for experiences we added in these test cases. After a bit of tweaking we came to a better number for the distance we considered a match for postcode, MCC and when we had both pieces of information.

V3 – How we match now

We knew we needed more information we could match on so we turned to a few different IDs: the merchant ID, terminal ID and sub-merchant ID. We knew these were sometimes duplicated or would change for some merchants (for example if a merchant was using a curve terminal or if they got a new card terminal). We use these three IDs along with the description, postcode and MCC to match experiences by giving them a score. And this If the transaction meets the threshold we've set, we would consider it a match. With this implemented, we have matched 99% of Local Experiences, and not yet had an incorrect redemption at an experience we don't offer. In the rare case that we don't catch one, we add the data that we missed the first time to ensure we don't miss it again.

Again we pursued test driven development to ensure we had appropriate scoring for each of the different fields. For example we new postcode was a more unique field than MCC, it was given a score of 0.5 for a match instead of MCC which was given 0.1.

Here’s a test for the updated version of the matching:

Yonder Treats are a series of smaller experiences, like a coffee or pint, and we typically have around 40-50 locations for each treat (for example, we had over 50 pubs for Yonder Pints). Each location needs matching independently, which provided an additional challenge, as we needed separate matching records for each location of an experience, and then to find the closest match for each transaction to determine whether it was an experience or not. There's also a lot of data we need to manually find so we can match every different location accurately.

Making Local Experiences self-serve

On top of all the above, we had to ensure that no engineering work required to set up new experiences. The nature of our rewards program means it’s changing often, so an engineering bottleneck significantly impacts the customer value if we can’t move quickly.

It starts with our team settling on the rich content, the redemption value and fair use limit for each Local Experience partner. We use a CMS to house our experiences content, but also found it useful for containing the matching information. Once we’ve added a new experience, Dato sends a webhook and the experience gets added to the backend database.

We also need to be able to see the experiences before they go live to check they look right and test the matching works, so we have a couple of options that change who sees experiences and, more importantly, who can redeem them. Through our CMS, we can control the status of experiences, who can see them (staff or members), and schedule experiences to go live in the future at a date we set. This gives us flexibility to test Experiences in the app before a member sees them. In the future we’d love to add a testing group of customers who can help us test experience redemption before it goes live to our wider customer base. The CMS gives us the freedom to do all of this easily.

Harry Ford
Harry Ford
Senior Backend Engineer