I spent a while building a calculator that prices 20 marketing tools at whatever scale you type in. The UI was the easy part. The hard part was discovering that the thing I was trying to model does not have the shape everybody assumes it has.
Almost every pricing comparison you have ever read — including the ones I wrote before this — treats a SaaS plan as a number. Mailchimp: $13. That number is a lie of omission in at least three separate ways, and each one is a data-modelling problem before it is a consumer problem.
Here is what I got wrong, and the schema I ended up with.
1. It is a step function, not a rate
The instinct is cost = rate × units. Linear. Interpolatable. Easy to put in a table.
It is not linear. It is a staircase, and the interesting part is the height of the steps.
Klaviyo's ladder runs $20 at 500 profiles, $45 at 1,500, $100 at 5,000, $150 at 10,000 — then $225 at 11,500. Fifteen per cent more contacts, fifty per cent more bill. At roughly 13,000 profiles you are paying about double the 10,000 price.
If you model this as a rate, you will interpolate straight through the most expensive thing in the dataset. The step is not noise around a trend line. The step is the pricing strategy: put the discontinuity exactly where a growing customer is most committed and least likely to migrate. The full ladder.
The correct evaluation is a bisect over sorted breakpoints, not a multiply. Obvious once you say it out loud, and absent from essentially every comparison table on the internet.
2. Every tool meters a different quantity
This is the one that broke my first schema.
I wanted a column called contacts so I could compare eight email tools at 10,000 contacts. But the tools are not counting the same thing.
- Brevo bills on emails sent. Its position on that chart depends entirely on how often you send. Assume 4 sends a month and it is cheapest by a distance; assume daily sends and it is not.
- Klaviyo bills on profiles stored — including people who have never opened anything and never will.
- Mailchimp bills on contacts you have not manually archived, which means unsubscribed and bounced addresses stay on the meter until you go and remove them, and the same person in two audiences counts twice. Details.
- MailerLite and Kit bill on subscribers.
So "10,000 contacts" is not an input. It is a workload assumption that has to be projected onto each tool's own meter, and that projection is a modelling choice you are obliged to expose rather than bury.
You can tell an honest calculator by whether it has a footnote. Mine does: "Brevo bills by emails sent (assumes ~4 sends/mo)." That sentence is not a disclaimer, it is a load-bearing part of the result. Every calculator without one is either hiding an assumption or is wrong.
3. Some meters are not a function of your size at all
Ahrefs bills report generation against a credit allowance. Exports, deep crawls, large reports — same pool.
This one genuinely does not fit the schema, and I want to be straight about that rather than fake a number. Seats and contacts are properties of you: I can ask how many you have. Credits are a property of how hard you work in a given month. Two identical companies on identical plans get different bills because one of them onboarded a client in March.
The practical consequence for a user is "price the tier your busiest month needs, not your average month." The consequence for the model is that credit-metered tools get a range and an explicit caveat, not a point estimate. Why the credit meter is the real price.
The schema I ended up with
// What almost every comparison table assumes
type Naive = { tool: string; pricePerMonth: number }
// What the data actually is
type Tier = { upTo: number; monthly: number } // a step, not a slope
type Plan = {
tool: string
meter: 'contacts' | 'profiles_stored' | 'emails_sent' | 'seats' | 'credits'
countsInactive: boolean // Mailchimp: true. Klaviyo: true. Not a footnote.
dedupesAcrossLists: boolean // Mailchimp: false.
tiers: Tier[] // evaluate by bisect
seatsIncluded: number // Semrush ships exactly 1
extraSeatMonthly?: number // +$80 on Guru, +$100 on Business
monthlyMultiplier: number // 1.19 Semrush, 1.71 Pipedrive, 2.11 Close
predictable: boolean // false for credit meters — return a range
}
const costAt = (p: Plan, units: number) =>
p.tiers.find(t => units <= t.upTo)?.monthly ?? Infinity
Four fields in there (meter, countsInactive, seatsIncluded, predictable) exist purely because the naive model is wrong, and every one of them corresponds to a real invoice somebody did not expect.
The billing period is a multiplier, not a discount
One more field worth its own paragraph. "Save 20% with annual billing" is framing. The number that matters is the inverse: what the flexible option costs.
Across the tools I priced, monthly billing runs an average 44% premium over annual. And the premium is steepest at the cheap end — Close charges 111%, Pipedrive 71%, Mangools 64%. The tools a small business can actually afford are the ones that punish month-to-month hardest, which is precisely backwards from who can absorb an annual commitment. Full dataset.
Store it as a multiplier on the tier price, not as a discount on the annual one. The direction you store it in is the direction your readers will think in.
An aside that annoyed me
While checking what already ranks for "email marketing roi calculator", I looked at the top ten results. Eight of them are companies that sell email software or email services.
That is not a scandal — vendors build good tools and they have the data. But it does mean that almost every calculator a buyer finds is built by someone with a position on the answer. If you are going to build one, that is the gap, and it costs you nothing but the discipline of publishing your assumptions.
If you build one
- Model tiers as breakpoints and evaluate by bisect. Never interpolate between them. The discontinuities are the product.
- Store the meter as a field, not as an assumption. The moment two rows in your table count different things, a single scalar comparison is invalid and your UI has to say so.
- Publish the workload. "10,000 contacts, 4 sends a month, 3 seats" is a result. "10,000 contacts" is not.
- Give unpredictable meters a range. A point estimate for a credit-metered tool is a confident wrong answer, which is worse than a hedged right one.
- Version your prices with a date. Everything above is mid-2026 and some of it is already stale. A price without a timestamp is a claim you cannot defend.
The reason none of this shows up in normal pricing pages is not that it is hard. It is that the naive model flatters the seller, and the accurate one takes a footnote to explain.
I run RealCostLabs, where I rebuild SaaS pricing from published rate cards and price it at usage levels real businesses actually hit. Figures above are from published rate cards in mid-2026; vendors change prices, so check before you buy.


Top comments (0)