Back

Referral codes, done a little differently

How we designed referral codes to shape behaviour. A look at how Neptune's short rotating codes work, and how they support the spirit of genuine recommendations.

James Gemmell, March 17, 2026

Most referral systems are pretty similar. Generate a code, store it somewhere, and when someone enters it later, look it up and work out who gets the credit.

That approach is perfectly fine, but it tends to push referral programs in a particular direction. Codes become permanent. They get reposted, collected, and passed around far beyond direct recommendations. Before long, the "referral code" starts behaving more like a generic coupon than a personal recommendation.

For Neptune, that is not really the point of a referral program. Our goal is to make it easier for one person to recommend Neptune to another, and keep the system aligned with that intent over time. That's led to a referral code design that's more opinionated than usual; short codes, automatic rotation, and a little cryptography to keep it operationally simple.

What we wanted from the system

We had a few clear goals in mind when we designed Neptune's referral codes:

  • Keep codes easy to share
    Referral codes still needed to be short, simple, practical to send in a message, and produce useful urls.

  • Remain aligned with genuine recommendations, where practical
    We wanted the system to support person-to-person sharing, rather than encourage permanent coupon-style distribution.

  • Avoid long-lived code stockpiles
    Codes should expire and rotate naturally, so referral code lists lose value over time.

  • Give users control
    If someone wanted to invalidate an existing code and start fresh, they should be able to do that immediately.

  • Keep the system operationally simple
    We wanted to avoid background jobs, cleanup tasks, and large lookup tables of active codes.

  • Make validation fast and predictable
    A submitted code should be cheap to check and easy to resolve back to the owning account.

The usual model

At a really high level, most referral systems have a stateful data flow looking something like this:

referral_code -> find in lookup table -> apply to account

A code comes in, the system looks it up, and finds the account that owns it.

Once you add expiry, auto rotation, or early refresh, that model grows a second stateful moving part:

iterate stored codes -> check expiry -> rotate / replace -> update stored state

Neptune's model

The Neptune system is a little different:

f(current time, referrer identity) -> referral code
g(current time, referral code) -> referrer identity, is valid

A Neptune code is not just a token that points to stored state, it carries enough information for the system to reveal the referrer directly.

That means the lifecycle of the code is derived, rather than managed, and the code can change with time with no moving parts.

How we map the referrer identity

Neptune referral codes are 13-character strings, something like HD53ST4W3DF6U.

Each code is generated from a small payload that includes two things:

  • the referrer identity: a unique identifier tied to the referral record, and by extension an account, and;
  • the day bucket: a time bucket with day granularity pegged off UTC

The payload structure looks like this:

upper                lower
+-------------------+------------+
| referrer identity | day bucket |
|      48 bits      |  16 bits   |
+-------------------+------------+

This payload is encrypted then encoded into the short 13 character base32 referral code that users share. We used a small 64-bit block cipher here, which helped keep the shareable code compact.

In pseduo code, the process to produce a code looks like this:

payload = (referralId << 16) | dayBucket // the referrer identity in the upper bits, and the day bucket in the lower 16 bits
code = base32(encrypt(key, payload))

So when the code comes back in, we can run the same process but in reverse:

payload = decrypt(key, decodeBase32(code))
referralId = payload >> 16 // upper bits
dayBucket = payload & 0xffff // lower 16 bits

This lets the system reveal the referrer identity and the account to apply credits to directly from the code, rather than treating the code as an opaque string that has to be searched for first.

That's great because it's fast, but it's also useful operationally. These codes carry time and identity together, so it becomes easy to make them rotate cleanly and expire predictably.

How the codes rotate

Neptune's referral codes are valid for a 7-day window. Within that window, the code stays stable and when the window ends, it rolls to a new value automatically.

There are no background jobs doing this. The code is derived from the current time window, anchored off the referrer identity's creation date, so when the 7 day window ticks over, the code ticks over too. It's a bit like a TOTP code in that time is part of the input, but instead of proving knowledge of a secret, Neptune’s code can be decoded back into the underlying referral identity.

The pseudo code for how the code rotates looks like this:

windowStart = identityCreatedAt + floor((now - identityCreatedAt) / 7d) * 7d
code = getCode(windowStart, referralId)

And returning to the code production snippet earlier, we now see how getCode works, too:

// how getCode works, basically
dayBucket = floor(unix(windowStart) / 86400)
payload = (referralId << 16) | dayBucket
code = base32(encrypt(key, payload))

In reverse, we can decrypt and decode to assert its age:

// how we validate a code's age, basically
payload = decrypt(key, decodeBase32(code))
codeDayBucket = payload & 0xffff
currentDayBucket = floor(unix(now) / 86400)

ageInDays = currentDayBucket - codeDayBucket
valid = ageInDays >= 0 && ageInDays <= 6

With weekly rotating codes, you might ask why the time component is stored in days, not weeks, hours, minutes or seconds.

The reason is that the code has to fit both a referrer identity and a time bucket into a compact 64 bit payload. If the time component becomes more precise, it consumes more bits. That leaves fewer bits for the referrer identity, which increases the chance that two accounts end up with the same code.

Day buckets give us a good balance. The codes still rotate predictably, and expire cleanly, while leaving plenty of space for the referral identity so collisions stay vanishingly unlikely.

The 64 bit block results in 8 bytes of ciphertext, which encodes neatly into the 13 base32 characters. It keeps the code short enough that it's practical to share, but with a time encoded validity period. Old lists of referral codes decay on their own and shared codes have a natural lifetime. That does introduce a bit of tension around expiry, so we let users refresh their code on demand and start a fresh window immediately.

Showing the refresh UI

All of this together allows us to enforce a referral code timeline like this:

Day 0  -> code A begins
Day 3  -> still code A
Day 7  -> code B begins automatically
Day 10 -> user refreshes, code C begins immediately
Day 17 -> code D begins automatically

What "refresh early" actually does

Refreshing early does not extend the current code. It creates a new referral identity and starts a fresh 7-day window from that point.

That matters because it gives the referrer some control. Changing your Internet provider can be big decision, and people might need a few days to mull it over. In these situations, a user can refresh a code and get a new 7 day window on the spot. Since we only want to allow a single valid referral code per user, an early refresh immediately invalidates the previous code, even if it would otherwise still fall within a valid time window.

This means the referral system is not just checking whether a code is structurally valid and recent enough. It's also checking whether that code still belongs to the account's current referrer identity.

The whole validation check

Putting the whole code validation logic together in plain english then looks something like this:

  1. decode the code
  2. confirm it falls within a valid window
  3. resolve which referral record it belongs to
  4. confirm that referral record is still the current one

And to achieve this, Neptune only needs to store a single referrer identity record for the account, and run a web server.

Why we landed here

This is not the only way to build a referral system, and it is probably not the most common or even the right choice for most companies. Persistent codes backed by a database lookup are a completely reasonable solution.

Neptune just wanted something a little different; a system that was lightweight, predictable, and better aligned with the idea of referrals as actual recommendations between people. That led us to a design with a few useful properties all at once:

  • The codes are short and easy to share.
  • They rotate automatically, with no background cleanup.
  • They do not rely on a giant global mapping of active tokens.
  • They let the product reinforce how the referral program is meant to be used.

And that last part is really the point.

A lot of product policy lives in rules, terms, and support decisions. Sometimes that is enough, but sometimes it helps when the system itself nudges behaviour in the right direction. Referral codes turned out to be one of those cases.

They are a small feature, but they sit right at the intersection of growth, trust, abuse, and user behaviour. Once we looked at them that way, it felt worth designing the system so the behaviour came from the implementation, not just the policy page.