Distributing campaign-specific promo codes is typically done in one of these ways:
- Popping a key off a precreated stack of unique codes, like the Excel sheet a client sent me the other day with values like
MT2017136A
,MT2017298B
,MT2017189F
, etc. - An autoincrementing integer, perhaps padded with zeros as an alphanumeric string to look more secret-code-like:
000001
,000002
,000003
. - The next non-duplicate generated by a (pseudo)random number generator, like JavaScript's
Math.random()
or the seedable PRNG you can find in most programming environments. This is harder than most people think. If you don't do it right, you'll send duplicate codes that didn't show up in testing. I'll deep dive in another post, but you must think of randomness and uniqueness as antithetical goals or, at best, unrelated concepts.[1] - Choosing a number in a certain range (say 100001-200000) based on the next output from a PRNG. This does nothing to solve the problems of (3) above, it actually makes them worse.
As you might gather from my tone, the naïve approaches in (1) and (2) are less prone to error than trying to do (3) or (4) correctly without understanding how random sequences actually work. You may not like the basic output you get from (2), but that's better than an angry customer. :)
But (1) through (4) all suffer from a common problem: they require that a separate counter, or index position, is maintained so you know which one to give out next. Even the simple idea in (2) of “just” incrementing the number requires that you know the last number!
This is a major encumbrance when using a webhook (as you would with Marketo) to dole out codes, since webhooks are inherently stateless: each Call Webhook step doesn't have any relationship with the the previous call.
FlowBoost has its FBCounter module, which allows you to update a counter that's shared across webhook calls (commonly used to distribute event registrations, it can actually be used to count anything you want).
But maintaining a counter is, as it turns out, overkill. In a Marketo environment, it's possible to have a webhook create a unique alphanumeric key per lead, per campaign, without using a counter. Let's see how!
Leveraging Marketo Unique Code
Every person in a Marketo database has an automatically generated alphanumeric field called Marketo Unique Code
.
This field, with values like YAE5ZV, BEZEXB5, and WC6TJB, can be used as a base-36 number for further calculations.
Base-36 is awesome
For those whose last math class was long ago (doubt it was longer ago than mine, except emotionally!), base-36 notation uses 36 alphanumeric digits to represent the numbers 0-35 in a single digit:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
It's case-insensitive, so there's no difference between ABC123 and abc123 in base-36. Depending on the context, you might see the letters printed in in lowercase (like when you use JavaScript's toString(36)
) or in uppercase (like when you see an airline reservation number PX9JD7 — hint, you already use base-36 all the time!) but they're exactly the same value.[2]
Two big advantages of base-36 notation over traditional decimal (base-10):
- it keeps the number of string characters much shorter than decimal: base-36 ZZ (2 chars) can represent the decimal 1295 (4 chars)
- it makes strings, for want of a better expression, more secret-code-like as opposed to number-like: coupon code LFLS probably looks cooler than coupon code 1000000 (though they're the same number)
So we just use the Unique Code? That's your big news?
Nope, not that simple. If you use the Marketo Unique Code as is, it won't be unique across promotions. (And I wouldn't spend a whole post on that!)
You can add a promo-specific prefix (or suffix) like DEC17-{{Lead.Marketo Unique Code}}. While that does make a unique code per-promotion, the problems are:
- You expose the lead's Unique Code value in what to me is a less-than-professional way.
- You allow a lead to predict their own (or any other lead's) promo code if they know the Unique Code. This may may seem insanely nitpicky, but I really don't like the idea that you can know your friend/coworker's discount code for a new promo just because you know their code for the last promo.
- Exposing the Unique Code value is, in an extremely strict view, insecure, since that same code is used for pURLs. Leakage would allow someone to impersonate someone else on a pURL-enabled Marketo LP (thus seeing other token values) without knowing their email address.
What I'm instead going to show is how to use the Unique Code as a “seed” of sorts to create unique per-promotion codes that don't reprint the lead's Unique Code.
Preserving uniqueness
Rule of thumb once you have a unique number: treating that number as a string and slapping other numbers onto the beginning or end results in losing uniqueness.
As an example, if you have a known-unique number between 1 and 10000, say 123, and you prefix it with a random number between 1 and 10000, you might get 99123, 7123, 128123, etc.
But random 99 + unique 123 = 99123 is indistinguishable from random 991 + unique 23 and random 9912 + unique 3! In other words, by simply concatenating the numbers as strings, you can no longer guarantee the uniqueness of the final code.
To solve this, you must convert the unique number to a string, then left-pad it with 0
up to the maximum number of string digits. For example, a number between 1 and 10000 can be output as up to 5 characters, so you'll prepend up to 4 0
characters. I've provided a sample JS pad()
function in the Notes.[3]
When you pad out the unique part first, then prepend another random number-as-string, you get results that preserve the original uniqueness like:
"99" + pad("123",5) = "9900123"
"991" + pad("123",5) = "99100123"
"9912" + pad("123",5) = "991200123"
You can convert those numeric strings to numbers proper and be assured that they're unique. You'll notice they're pretty long numbers (left-padding internally can definitely do that!). Here's where outputting in base-36 comes in handy, since 9-digit 991200123 is only 6-digit GE4UWR in base-36 and looks properly secret-code-like.
So what's the secret sauce for codes that are unique per-promo and per-lead?
- Convert the Marketo Unique Code from base-36 to decimal.[4]
- Convert the decimal number to a string.
- Pad the decimal number-like-string to 11 digits, zero-filling from the left.[5]
- Prepend a random number in a certain range, converted to a string. There's a JS function in the Notes[6]:
getRandomInt(1,1000)
. Or you could useFBRandom
for more control (which I'll dive into in another post). - Convert the final number-like-string back to a number, then back to base-36.
Sample of the process:
Original Marketo Unique Code: XYZPDQ
Converted to decimal: 2054156606
Padded to 11 chars: 02054156606
Random number: 456
Concatenated random + unique 45602054156606
New base-36 promo code: G5XALMUKE
Run again for the same lead, but a different promo:
Original Marketo Unique Code: XYZPDQ
Converted to decimal: 2054156606
Padded to 11 chars: 02054156606
Random number: 678
Concatenated random + unique 67802054156606
New base-36 promo code: O17U0G472
And another lead, for the second promo above, that happens to get the same random number part:
Original Marketo Unique Code: XYZPST
Converted to decimal: 2054157149
Padded to 11 chars: 02054157149
Random number: 678
Concatenated random + unique 67802054157149
New base-36 promo code: O17U0G4M5
As you can see, the promo codes are guaranteed unique across leads and do not reveal the underlying Unique Code field to the end user.[7]
Pretty cool, eh?
Yeah, but…
The astute reader will note that if there's always a random number between 1 and 1000 prepended to the unique code, that random number is not guaranteed unique across promos (it can't be, if we keep our original objective of being stateless, as each Call Webhook has no idea of the outcome of the last call). The algorithm is thus not yet guaranteed to generate unique codes for the same lead across promos.
Yep! Fix this by using a different range of 1000 for each promo. Everyone in the first promo gets numbers getRandomInt(1,1000)
, next promo gets getRandomInt(1001,2000)
, one after that getRandomInt(2001,3000)
. Then there can't be a duplicate result for the same lead, nor across leads.
The final function
After including the helper functions pad
and getRandomInt
, this is all you need as your FlowBoost payload (or wherever else you're running JS):
function getPromoCode(mktoUniqueCode,campaignNum) {
var uniqueDec = parseInt(mktoUniqueCode, 36),
paddedUnique = pad(uniqueDec, 11),
random = getRandomInt( campaignNum * 1000 + 1, (campaignNum + 1) * 1000),
promoString = random.toString() + paddedUnique;
return parseInt(promoString).toString(36).toUpperCase();
}
var promoCode = getPromoCode({{Lead.Marketo Unique Code}}, {{My.Promo Codes Set}});
Where campaignNum
is an integer that sets the range of random numbers (1 = 1001-2000, 2 = 2001-3000, etc.). In the example, I've stored that value in the program-level token {{My.Promo Codes Set}}.
But as mentioned above, if you don't want to worry about this you could use a promo-specific prefix like DEC17- and pass 1 as the campaignNum
all the time. You could potentially have duplicates in the random number part for the same lead across promos, but that would be smoothed over by having a unique prefix for the promo.
Limiting the number of codes
If you're not concerned about limiting the total codes you give out (just limiting them to one per email address) then you're all good with the above method. No need for a counter: every code is guaranteed unique, and just make sure that a lead whose Winter 2017 Promo Code field is already filled in doesn't get another one (i.e. doesn't run the webhook).
In my experience, the “First 500 signups get a promo code” is a little squishy. In reality, you may mean “All the signups by end-of-day today.” I mean, unless you're really trying to gamify your marketing, or the coupons cost you a ton to redeem, disappointing interested folks isn't so cool, right?
But if you definitely need to limit the number of total codes given out, you'll have to use FBCounter or some equivalent technology (not that there is any, heh). Something like this to check the counter ahead of time:
FBCounter.autoSave( 'promo_codes/december_2017', {{Lead.Id}} )
.then(function(newEntry) {
if( newEntry.index < 500 ) {
// under 500 codes distributed
// run your getPromoCode() algorithm to give out a new one
} else {
// error, 500 codes were given out already
}
});
Notes
[1] The core confusion is that people think a PRNG's period refers to the minimum length of a non-duplicated set of numbers, when it actually refers to the maximum length of a sequence of numbers before the whole sequence repeats — regardless of whether the sequence has dupes.
Example: 1,2,3,2,5,11,9,1,2,3,2,5,11,9,1,2,3,2,5,11,9...
repeats in blocks of 7, but there are already dupes at positions 2 and 4! The period might be 7, but that doesn't mean the first 7 numbers are unique.
So they think “This thing will generate a random list of 32 billion numbers without repeating a single one” and figure they'll never have more than a few million whatevers so they're fine. But in actuality there could be repeated numbers at indexes 10 and 1000, they just might not test enough to see what's at 1000.
Again, uniqueness and randomness aren't the same thing at all. Even in casual English, this is true: if the same weirdo from the party is also your barista, that can be So Random — not despite the repetition but because of it!
[2] A theoretical base-62 system, on the other hand, could be case-sensitive, using numbers 0-9 and both letter cases independently:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnoprstuvwxyz
But you're unlikely to see a base-62 string as a coupon code, registration number, or anything user-facing, because of the huge probability of user error. Imagine if airline check-in kiosks were case-sensitive and stressed-out passengers didn't realize aBC9zZ had to be typed carefully!
[3] One of those things there's a zillion ways to do, but here's one that should be both comprehensible (no tricky one-liners) and comprehensive (allowing left- and right-pad, multi-character placeholders, etc.):
function pad(str,length,placeholder,anchor){
var argInfo = {
length : { default : 0 },
placeholder : { default : '0' },
anchor : { enum : ['left','right'], default : 'left' }
}
// coerce args a bit
var str = str.toString(),
length = length || argInfo.length.default,
placeholder = placeholder || argInfo.placeholder.default,
anchor = argInfo.anchor.enum.indexOf(anchor) != -1
? anchor
: argInfo.anchor.default;
// an array of placeholder chars to fill underflow
var fillCount = Math.max(length - str.length, 0),
placeholderCount = Math.ceil(fillCount / placeholder.length),
fillChars = new Array(placeholderCount)
.fill(placeholder)
.join("")
.substring(0,fillCount);
// pad appropriate side
switch (anchor){
case 'left':
return fillChars + str;
case 'right':
return str + fillChars;
}
}
[4] It's built into JS:
parseInt({{Lead.Marketo Unique Code}},36)
[5] 11 decimal digits is based on the maximum decimal value that can be represented in 7 base-36 digits. ZZZZZZZ which is the longest base-36 Marketo will calculate. ZZZZZZZ36 is 7836416409510 which is 11 number-as-string chars.
[6] Rather than writing my own function for this one, I'll use the horse's mouth at MDN:
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
[7] To be clear, this is not an encryption algorithm, it's a coupon code encoding algorithm! :) The goals are usability (relatively short, case-insensitive codes), uniqueness (no confusion among customers or campaigns), and (for most mortals) lack of predictability. It's not like a skilled cracker couldn't reverse the algorithm if they had an assortment of valid codes to run forensics on (or, ah, just read this blog post). But that still wouldn't put a valid coupon code value on their lead record, which is the important part: they aren't getting away with free stuff.
Additional random note
For those few that care, the maximum base-36 number that can be safely calculated in JavaScript is the 11-digit 2GOSA7PA2GV (Number.MAX_SAFE_INTEGER.toString(36)
). In Java, it's 1Y2P0IJ32E8E7 (Long.toString(Long.MAX_VALUE, 36)
) which is 2 alphanumeric digits longer.
However, these limits don't affect values that have already been converted to strings! As we learned in my deep dive into Integers in Marketo vs. SFDC vs. various programming environments, you need to stay up on computational limitations, but once you've computed a number and converted it to a string for someone to type into a form, you're not in the world of numbers anymore.