Rebuilding how Helio charges, from the pricing page to the bill
- Role
- Design and frontend, across the marketing site and the product
- Working with
- A backend engineer on the plan catalog and the Stripe wiring, and an engineer who carried the review rounds
- Timeline
- Mid-July to early August 2026, four merged PRs
- Stack
- React, TypeScript, Tailwind, Next.js, Stripe, nine locales
Where it started: helio.im/pricing, both versions running.
The retired page is the real retired component, restored from the commit that replaced it. The new one is the shared package the marketing site renders.
Helio charges in credits. The interface described them as a monthly allowance with a meter that drained, which is how a phone plan works and not how this product works. You can buy more credits, and your balance does not reset, so the meter was misdescribing people's own money back to them.
I changed the vocabulary before I changed any screen. Allowance became balance and meters became a wallet, and everything else had to be re-derived from that. Does the usage bar still exist? What happens to your balance when you cancel? (It stays. The old copy implied otherwise.)
The job started on the marketing site, so that is where the new vocabulary landed first. It had five cards in a three-and-two grid, a column labelled "usage" that nobody could decode, and discount numbers three to five percent better than what the backend would actually charge, so the page was overselling without anyone intending it to.
Three cards now, and the credits figure sits on the first line of each because it is the number people came for. The prices are set in a sans, not the display serif the rest of the page uses: Playfair's dollar sign carries two vertical strokes and at that size it stops reading as a dollar sign.
Then the people already inside
A pricing page only serves visitors. Everyone already inside the product who wanted to spend more money hit a different surface: a nine-row capability matrix in Settings, sitting next to booster purchases, redeem codes and subscription management. That put four unrelated jobs at equal weight on the screen you open to find out when you get charged next.
Settings, Billing. Both versions running.
Upgrading and managing are different states of mind. Someone who clicks Upgrade plan has decided already; someone who opens Settings wants to know when they get charged.
Upgrade intent now opens a dialog and Settings keeps one subscription card. The table itself was 587 lines, and retiring it took 3,193 lines out of the app across 38 files.
The dialog that replaced it
Real components and real mock data, running in the page. Click into it.
The cards in this dialog and the cards on the pricing page are the same component. A visitor picks a plan on helio.im and that choice carries into the app. It runs in nine languages, and I had the Chinese reviewed by a native reader after noticing the credit verbs had drifted into three different words for the same action.
Where money lives when nobody is thinking about money
Around the dialog, the prompts are layered by what they are for: a plan badge that is always there because it is identity, a card in the shell that appears only when the state needs an action, and a credit reading on the home screen. A healthy paying customer should not be nagged by any of them.
Real components and real mock data, running in the page. Click into it.
The middle layer earns its place by being conditional. On a healthy account the rail carries nothing. When the balance runs out, a card appears behind that icon with the number and exactly one action, Buy booster. It sits apart from the plan badge because the badge has to stay put and the warning has to be temporary.
Real components and real mock data, running in the page. Click into it.
Manus puts a centred pill on its home screen. I did not, because a pill in the middle of a quiet home reads as an ad unit, and the gap here was the number rather than another place to print the plan name. On a paid account in good standing the Upgrade half disappears and only the balance stays. It is built, tested and in the catalog; production never shipped the home slot it docks into, so it is not mounted yet.
Six decisions
| Decision | Chose | Over | Why |
|---|---|---|---|
| Where a blocked user lands | Billing, for all three reasons | Usage, for the two who only need credits | Running out is the moment upgrading makes sense. The booster is an escape hatch for power users, not the pitch. I gave up the shortest path on purpose. |
| In-app credit visibility | A capsule in the top right corner | A centred pill, the way Manus does it | A pill in the middle of a quiet home screen reads as an ad unit. The gap was the number, not another place to print the plan name. |
| What a healthy paid account sees | The balance, and nothing else | Balance plus a standing upgrade link | Selling to somebody who already bought is noise. Claude and ChatGPT do not do it either. |
| Labelling the credit tiers | No label on any tier | Best value on the largest one | All four tiers have the same unit price. The label would have been true of nothing. |
| A redeem code that fails | Keep what they typed | Clear the field | It is twenty-odd random characters pasted out of an email. Clearing it sends somebody back to their inbox at the exact moment they have been told no. |
| Why a code was refused | One message for every reason | Expired, used, disabled, spelled out | The backend refuses to tell them apart on purpose, so a script cannot farm the endpoint for valid codes. The interface should not leak what the backend is protecting. |
Where the credits actually go
Last came the other half of a wallet, and the surface this whole case study is named after. The balance card used to carry a "Monthly allotment" progress bar. It read as a phone plan, and it clamped at a hundred percent, so it was least accurate exactly when somebody had bought extra credits and their balance ran past the monthly grant. There is no denominator to show for a wallet, so the bar had to go before the rest of the card could be honest.
Settings, Usage. Both versions running.
Same spends on both sides. On the after, press Buy credits: the purchase dialog is real.
Under the bar sat the ledger, which had the opposite problem. It had five fixed columns, two of which the backend could only fill when it happened to know them: which teammate spent the credits, and which model. Both are optional in the wire contract, and the row rendered an em-dash when they were missing, so a real account's table was mostly em-dashes. A demo never showed this, because a demo's rows are complete.
What replaced it answers a different question: not how much of your allowance is left, but what you have and where it went, one row per task. The point is reconciliation, so that somebody looking at a charge can say which piece of work it paid for. The fixed columns went, and a row now carries only the facts that exist for it. An unknown model is simply left out.
Booster purchases used to be four buttons that threw you at Stripe with no step in between. They moved into a dialog behind the small outline button next to the number it changes, and the flow grew a middle. Pick an amount, see the total, then a button carrying the exact dollar figure and the word Stripe.
The thing nobody asked me to look at
While building the step that runs just after signup, I went looking for where a plan chosen on the marketing site gets picked up on the other side. The code that reads it was there, but nothing anywhere wrote it. The pipe had never been connected, so everyone who clicked Get Pro, signed up and finished onboarding had their choice dropped and landed in the product as though they had never picked anything. Those are the people you least want to lose, because they decided to pay before they had used the thing.
The fix is designed and sitting on a branch. It has not shipped, and of everything here it is the change I would most like to see land.
One small thing worth passing on. The prices on those cards roll when you change the cycle or the tier, and I tried to write that animation myself first, with Claude Code. Sliding the digits is the easy part. What eats the afternoon is everything around them: the width changing when a number gains a digit, the spacing between glyphs, keeping the dollar sign still while the figure moves under it. NumberFlow does all of it, and switching to it took less time than one round of tuning my own version had.