UPDATE: This is wrong see more recent comment below: Premium upgrades and downgrades
This is a bit TMI but I’m sketching out the spec for how to make premium upgrades and downgrades exquisitely fair since we make a big deal about that and I figure I might as well do it it publicly in case y’all want to kibitz or just to have more eyes on it. But for almost everyone it suffices to know that, as we say in the fine print, we just do the opposite of what your phone company would do. The fundamental principle goes like this:
You can jump on any plan with any payment frequency at any time. If you change your mind and upgrade or downgrade you won’t have wasted any money by doing so.
The only way to regret any premium purchase is to buy more than you actually wanted, like buying a year of Bee Plus when you only wanted a month of it. Or to buy Beemium when Bee Plus would’ve sufficed. Or buying lifetime and then dying within 7 years. (Technically you could replace “dying” with, say, solving your akrasia for good or something.) If any of those things happen, talk to us and we’ll make sure you (or your heirs) are happy.
(I think this part’s obvious but to spell it out again: By opting to pay for longer periods, like yearly or lifetime, you’re committing to having that plan (or higher) for that long. That’s the point of the slider – a discount in exchange for committing to a longer period. You can ask for a refund if the perks feel disappointing and we’ll say yes.)
But, mismatched expectations or poor planning aside, the point is that we want everything to be exquisitely fair automatically. With no need to talk to pesky humans. Or even nice humans like us.
For example, if you get yearly Infinibee and then downgrade to core Beeminder, you can do that immediately but you’ll still have Infinibee for a year until what you paid for runs out. Or if you get yearly Bee Plus and then immediately upgrade to monthly Beemium, that’s fine too. You’ll just pay $16 less each month for Beemium because that’s the nominal monthly value of the Bee Plus you already paid for (even though you paid less by buying a year of it). After that year runs out you’ll automatically start paying the normal price for the plan you’re on. You’ll get a credit for the unused portion of your old plan. (If the old plan was lifetime, you’re credited the full amount you paid!) You’ll just draw from that credit when paying for the higher plan until it’s used up.
Corollary: If lifetime Infinibee would be a nobrainer if there weren’t those bigger, shinier plans off to the right, then first grab the lifetime Infinibee. Now any other plan will be $4/mo cheaper for you forever. You can always get the full amount back as a credit to apply to any other plans if you change your mind.
Ok, everyone but the uebernerds can stop reading now.
Algorithms and data structures and pseudocode, oh my!
Ooh, Discourse can mercifully hide gory details till you click here
First some handy constants:
DIY = 365.25 # days in year
SID = 86400 # seconds in day
DIM = DIY/12 # days in month
SIM = DIM*SID # seconds in month
R = 0.03 # monthly interest rate aka discount rate for the discount slider
(The discount rate and some of the relevant math is discussed in our post on the exquisitely fair discount slider.)
Users have 2 new fields:

credX
is a number of dollars from which to pay for premium plans before actually charging 
credT
is the timestamp that the credit was last adjusted
Whenever the credit is adjusted you compute the current uptothesecond credit amount with the standard equation for continuously compounded interest:
credX * exp(R*(nowcredT)/SIM)
Let’s make a handy function out of that to give the net present value as of time t2 of what was $x at time t, assuming our global monthly discount rate R
:
npv(x, t, t2) := x * exp(R*(t2t)/SIM)
So a user’s uptothesecond amount of credit is npv(credX, credT, now())
. Then you adjust it (add new credit, subtract credit being used) and store the new amount in credX
and set credT
to the current timestamp.
We can make a function to add $x to user u’s credit like so:
# Add $x to user's credit balance at time t (default now)
addcred(u, x, t=nil) :=
if t==nil then t = now() # unixtime in seconds
# this works even if t happens to be earlier than u.credT  mathemagic!
u.credX = npv(u.credX, u.credT, t) + x
u.credT = t
There are Plan objects that have the following fields:

name
is one of: “core”, “infinibee”, (“beelite”), “beeplus”, (“planbee”), “beemium” 
nom
is the nominal monthly amount of the plan ($0 for core thru $32 for beemium) 
freq
is 1 for monthly, 6 for semiannually, 12 for yearly, 24 for biyearly, etc, and 1000 for lifetime 
cmult
is the coupon multiplier, like 0.90 for 10% off (default: 1) 
paidtil
is the timestamp of the next charge (updated as+=freq*SIM
when you pay)  (
chamt
is the charge amount eachfreq
months)
The chamt
field is in parens because it’s cached and recomputable from the other fields like so:
chamt = nom * cmult * fmult(freq)
where the fmult
function is defined like so, taking the payment frequency as a number of months:
fmult(n) := (exp(R)  exp(R  n*R)) / (exp(R)  1)
fmult(1000) := exp(R) / (exp(R)  1) # limit as n goes to infinity
But, to make it easier to specialcase that ugly infinity=1000 thing, we actually use this version with an optional second argument:
# Frequency multiplier for n months of a plan, with fraction p remaining
fmult(n, p=1) := (exp(R)  exp(R  n*p*R)) / (exp(R)  1)
fmult(1000, p=1) := exp(R) / (exp(R)  1) # limit as n > infinity, p irrelevant
For example, if your payment frequency is every 2 months then fmult(2)
yields 1.97, slightly less than paying twice as much as the monthly amount every other month. If you were paying every 100 months, fmult(100)
yields 32 instead of 100, which is a 68% discount. Note that calling, say, fmult(12, .5)
is the same as fmult(6)
so that seems dumb to bother with the second argument. We do it so that when we call, say, fmult(1000, .1)
we can notice the special case that that’s actually lifetime, not paying every 100 months.
(Yes, we myopically are using “1000” instead of “infinity” for lifetime plans. But don’t worry, we are so on it! That’s 83 years and we have it on our calendar to reimplement Beeminder in a more mathy language that handles infinity nicer by 2096 August 12. Of course we already specialcase it so what you pay for a lifetime plan is exactly the NPV of the perpetuity of the nominal amount monthly forever.)
Downgrades
Every user has a Plan object, daplan
(say it with a Chicago accent). If you downgrade then a second Plan object, downto
, is created for you which waits to take effect when daplan
runs out. We ignore downto
until it’s time to charge for daplan
again, at time daplan.paidtil
. Then we notice that there’s a downto
plan and we replace daplan
with downto
and start charging for it immediately.
Upgrades
When an upgrade from daplan
to upto
happens you just add the value of the remainder of daplan
to the user’s credit before replacing daplan
with upto
. First define a function to compute the remaining value in a plan at time t:
valat(p, t) := p.nom * p.cmult * fmult(p.freq, (p.paidtilt)/(p.freq*SIM))
And then effect the upgrade:
t = now()
addcred(u, valat(daplan, t), t)
daplan = upto
Whenever it’s time to actually charge the user, including immediately after doing the above for the upgrade, we use this:
# Charge user u $x, first applying as much credit as possible
premiumcharge(u, x) :=
t = now()
c = npv(u.credX, u.credT, t) # uptothesecond credit amount
if c >= x
c = x # and don't actually charge anything
elsif x  c < 1 # $1 being the least we can charge a credit card
actually_charge_user(1)
c += 1  x
else # wipe out the credit and actually charge the rest
actually_charge_user(x  c)
c = 0
u.credX = c
u.credT = t
And call it like this:
premiumcharge(u, daplan.chamt)
Philosophical and Implementation Notes

If credit is nonzero then display it in the UI as
npv(credX, credT, now())
. If credit ever goes negative somehow then airhorn ourselves. Probably a bug. 
The UI shouldn’t let you downgrade from a lifetime plan. For nonlifetime, the plan being downgraded to should say “Downgrading to this in __ days. [CANCEL]” and the current plan should indicate it’s the current plan like usual. Everything but the cancel button should be grayed out. You have to cancel the downgrade if you want to downgrade or upgrade to anything else.

Actually, I think we can simplify the implementation and the consistency and the user learningness by having no special case for lifetime except to say “infinity days” instead of the live countdown. So you can downgrade but it will end up being a noop since it will never take effect and the user will understand that because of the “in infinity days” and because there’s no other allowed action than to cancel that. Elegant! And I think that means there’s no lifetimerelated cruft in any of the code except for tweaks to some displayed strings in the UI and the limiting case in the
fmult
function. 
Credit balances are only for premium payments, never pledges. No stingdilution!

Credit balances pay an exorbitantly generous interest rate – the same one that the discount slider uses, 3%/month or 36%/year. Interestingly (tee hee) you could, say, spend $541 for lifetime Bee Plus, upgrade to monthly Beemium and cancel it, leaving yourself a credit of $509 which would earn you $15/month in interest. (Compounding continuously, of course.) Which means you could readd Bee Plus, paid every 7 months, and basically live off the interest, not touching the principal. Or maybe it doesn’t quite work because you lose a chunk of the principal by paying for 7 months at once. But it’s a pretty sweet deal regardless! But not, I think, so sweet that Beeminder’s leaving too much money on the table. To exploit any of this you have to have lots of faith in us and cough up a lot of money up front. Which is genuinely valuable to us. So it should all still be exquisitely fair.

Fun fact: If you got 7 years of Bee Plus, that would cost $498. If you waited 3.5 years and then upgraded, $388 of that $498 would be left as a credit. Why so much more than half? Because when you pay for 7 years up front, the second 3.5 years are more valuable than the first 3.5 years. Timevalue of money. In general you always get back more than the simple arithmetic would suggest. Like if you buy a year and upgrade to something else after a month, you get back as credit slightly more than 11/12 of what you paid.

Translogging: CRE for addcredit, PLA for up/downgrade (including free plan)

Big UI problem mostly orthogonal to all this: reassuring the user that if they have a lifetime or yearly plan that it’s ok to upgrade to a higher plan at a lower frequency. Maybe: “X months free by applying credit from your current plan”. Without credits the idea is to just quote a cheaper price but people’s loss aversion kicks in and they’re still paranoid about losing what they have. With credits they actually do lose it but maybe the tradein value, if made explicit, will be compelling? This wants some more thought with popup confirmations/explanations so that users can fearlessly be like “sure, I’ll try a bump to Beemium for a month”. What if the upgrade button said something slightly cryptic but intriguing/nonscary like “apply magic upgrade credit” and that popped up a popup that spelled it all out, including how many months free you can get of the newer plan by applying the remaining credit from your current plan?

The X in “X months free” is calculated as follows. First define a function to compute how long you can get a new plan for with a given amount of credit:
# How long can you get a plan w/ nominal monthly cost $m for if you spend $x now?
upfree(m, x) :=
pow = m / (m*exp(R)  x*exp(R) + x)
if pow <= 0 then return 1000 # $x is enough for lifetime
return (R + log(pow)) / R # show with 2 sigfigs
That’s roughly the inverse of fmult
in that fmult
gives the cost of buying n months at once while upfree
gives the number of months you can get by spending a given amount of money. So now we can compute the X in “X months free”, given a nominal monthly cost m of a new plan and coupon multiplier (from the query string) c like so:
upfree(m*c, valat(daplan, now()))
Scratch area
How I previously spec'd this before deciding it was all wrong
If you upgrade then a new Plan is created and daplan
is set to it (upgrades take effect immediately) and your previous Plan is stored as upfrom
. It affects the price of your new plan until it runs out.
Seeming monkey wrench that’s not actually that bad: To do this version right, upfrom
needs to be a list of plans. When you upgrade you add your current plan to the list. When it’s time to charge for the new current plan (daplan.paidtil == now
), you first remove from the list any plans with paidtil in the past (p.paidtil <= daplan.paidtil
). Then you pick the maximal p.nom*p.cmult*fmult(daplan.freq)
over all the plans p
and reduce the payment for daplan
, i.e., daplan.chamt
, by that amount.
If there were just one upfrom
plan then the amount to charge at time daplan.paidtil
would be, assuming upfrom.paidtil > daplan.paidtil
:
daplan.nom * daplan.cmult * fmult(daplan.freq) 
upfrom.nom * upfrom.cmult * fmult(daplan.freq) [eq1]
(Note that fmult
takes daplan.freq
in both places. We use the new plan’s frequency to determine how much credit you get from the lower plan you’re upgrading from.)
When upfrom
runs out then the user only has daplan
and we go back to charging simply nom * cmult * fmult(freq)
as stored in daplan
.
(It shouldn’t be possible for the amount to charge to be negative, as long as you never “upgrade” to a plan that’s cheaper. But the code should have an assertion and airhorn us just in case.)
How to actually decide what to charge when…
If upfrom
and downto
are nil then just charge daplan.chamt
at time daplan.paidtil
.
(Else assert that exactly one of upfrom
and downto
are nil.)
If upfrom
then:
 charge the amount in [eq1] at time
daplan.paidtil
daplan.paidtil += daplan.freq*SIM
 if
daplan.paidtil >= upfrom.paidtil
then setupfrom = nil
If downto
then:
Wait, yikes, this has gotten hairy to do Really Right. Consider the use case of getting yearly Infinibee, then upgrading to semiannual Plan Bee, then upgrading again to monthly Beemium. [There were other, less cornery cases we were getting wrong too but this is the one that convinced me that there was no patching the spaghetti and we had to rethink it.] We either have to convey in the UI that you can’t do that or else store a list of plans you’re upgrading from [not as bad as it seemed – I think this scratch area has the right spec for that now, but @bee says it’s still going to lead to spaghetti] so that while on Beemium you can be credited with the Bee Plus plan you’ve got for the first 6 months and then the Infinibee plan you have for the next 6 months.
I’m hashing this out on paper until I figure it out.
UPDATE: I’m pretty confident now in the creditbased version above!