The report was that the monthly total was off by a few units of currency. Not much — enough that someone who had added up their subscriptions by hand noticed the app disagreed with them.

We were storing amounts as floating point numbers.

Binary floating point cannot represent most decimal amounts

This is one of the oldest known problems in software and it keeps getting shipped, including by us.

A binary floating point number cannot exactly represent most decimal fractions. The classic demonstration is that adding 0.1 and 0.2 does not give you exactly 0.3. Each individual value is off by a vanishingly small amount, rounded away when you format it for display, so nothing looks wrong.

Then you add twenty of them together, apply a currency conversion, divide an annual amount by twelve, round for display, and the accumulated error crosses a threshold where it becomes visible. The total is off by one unit in the last place. Every individual line looks right and the sum looks wrong.

Amounts are now stored as integers in the currency's smallest unit. Cents, or whole won for a currency with no subunit. All arithmetic happens in integers, and formatting for display is the only place a decimal point appears. This is the standard answer and it is standard because there is not a good alternative.

Rounding is now explicit too. Dividing an annual amount by twelve does not come out even, and the app has to decide what to do with the remainder — consistently, in one place, rather than wherever the calculation happened to be written.

The monthly total has two correct answers

A more interesting problem, and one we got wrong in a way that was not a bug.

You have some monthly subscriptions and some annual ones. What is your monthly subscription cost?

Normalised: monthly amounts, plus each annual amount divided by twelve. This is the right number for "how much do subscriptions cost me per month on average" and it is the one you want when deciding whether a service is worth it.

Actual: what will actually leave your account this month. Monthly charges plus any annual renewals that happen to fall in this month. This is the right number for "can I afford this month" and it is spiky — the month your annual cloud storage renews looks very different from the one before it.

We showed one number and called it the monthly total. Users comparing it against their bank statement found it did not match, because we were showing the normalised figure and their bank was showing actual cash.

Both are now shown, labelled for what they are. The normalised figure is the headline, because it is the better basis for a decision about a service. The actual figure for the current and next month sits beside it, because that is the number that matters when you are looking at your balance.

Billing dates belong to the provider, not to you

Dates are the other half, and they carry their own set of traps.

Month lengths. A subscription billed on the 31st does not have a 31st every month. Providers clamp to the last day of the month, and the anchor stays the 31st for months that have one. Computing the next date from the previously computed date, rather than from the anchor, walks a subscription permanently onto the 28th after one February.

Timezones. A renewal date is a date in the provider's billing timezone. A charge that posts on the 1st in one timezone can appear on your statement on the 31st. For a reminder this matters: a notification the morning of a charge that already happened yesterday evening is not a reminder.

We store the billing date as a calendar date with the provider's timezone where we know it, and default to yours where we do not. Reminders are deliberately early rather than same-day, because the useful moment for "do I still want this" is before the charge, with enough time to do something about it.

Trial ends are the important date. A trial converting to a paid plan is the single most common thing people mean to cancel and forget to. It is also the one date where a reminder the day before is nearly useless — cancelling requires finding the account, working out how, and often waiting for a confirmation. The reminder is set well ahead, and it says what will be charged, because "your trial ends" without an amount is not enough information to act on.

Multiple currencies, and the conversion we do not do

Plenty of people pay for services in more than one currency. A total across them needs an exchange rate, and an exchange rate is a live number.

We do not convert. Totals are shown per currency, and a subscription in another currency shows the amount you are actually billed. This is less tidy than one number and it is honest: an app that shows a single converted total is showing a figure that was correct at whatever moment it fetched a rate, and it will not match your statement, which used the rate on the charge date plus whatever your card issuer added.

If you want a combined number you have the components. We would rather not invent the conversion.

Presets, and what they are not

The regional presets are a starting point — pick a service that exists in your market and get a name, a typical billing cycle, and a plausible amount, rather than an empty form.

They are explicitly not a claim about your plan. Prices change constantly, plans differ by tier and by region, promotional rates exist, and a preset is a reasonable default rather than a fact about your account. The form makes the amount and cycle the first things you confirm.

The right source is your bank or card statement. Build the list from what you were actually charged, not from memory — memory reliably omits the ones that matter, which are the small charges under names you do not recognise. Those are the whole reason to keep a list.

The widget

The home screen widget shows what is coming: the next renewal, the totals, and how many days away it is.

Its purpose is to be a prompt rather than a report. The number that changes behaviour is the one you see without deciding to look at it, and a subscription list you have to open is a subscription list you open twice a year.

Everything under the widget is subject to Android's update constraints — periodic refresh has a floor of roughly half an hour, and background work is deferred while the device is idle. For a subscription total this does not matter; nothing changes minute to minute. The widget updates when the data changes and on a slow schedule otherwise.

Setting the list up

Build it from a statement, not from memory. Three months of transactions. The forgettable ones are the point.

Confirm the amount and cycle against the provider rather than trusting the preset.

Record trial end dates before anything else. This is where the money is.

Note what each service is for, if the name is not obvious. A charge you cannot identify in six months is a charge you will keep paying.

Review before a decision, not on a schedule. Looking at the list because you are about to add another service, or because you are tightening a budget, produces cancellations. A monthly reminder to review your subscriptions produces a dismissed notification.