You have three tabs open: Stripe, Polar, and LemonSqueezy. Each one shows a different number. One includes annual plans at full value, one mixes one-time orders with subscriptions, and one is using a different exchange rate than the number you wrote down last week.
That is the normal multi-product setup for indie hackers now. One SaaS on Stripe, one developer tool on Polar, one template or plugin business on LemonSqueezy. The problem is not collecting revenue. The problem is getting one number that means the same thing everywhere.
If you want to track revenue from multiple products correctly, you need more than a dashboard screenshot and a spreadsheet. You need a consistent definition of MRR, processor-specific normalization, and one place to aggregate everything.
Why Multi-Processor Setups Are Normal
Founders rarely choose three processors on day one. They accumulate them because different products fit different tools.
| Product type | Best-fit processor | |---|---| | SaaS with trials, seat billing, or metered plans | Stripe | | Developer tools or open-source products | Polar | | Templates, plugins, info products, digital downloads | LemonSqueezy | | EU-friendly digital goods with tax handling | LemonSqueezy | | Subscription product with broad ecosystem support | Stripe |
A multi-processor stack is not a mistake. It is usually evidence that the founder kept launching things. But as soon as revenue is split across Stripe, Polar, and LemonSqueezy, the dashboards stop being directly comparable.
The Spreadsheet Trap
The first fix is always the same: export CSVs, copy them into Sheets, and write a few formulas. It feels responsible. For a week or two, it even works.
Then reality catches up. Billing intervals change. Refunds hit after the original charge month. A one-time LemonSqueezy order gets mixed into your recurring revenue tab. A Stripe customer upgrades from monthly to annual and your VLOOKUP still points at the old row. The spreadsheet becomes a fragile patch over three systems that were never designed to agree with each other.
The real problem with the spreadsheet approach is not that it is manual. It is that it silently drifts. You still get a total, but you stop being able to explain why it changed.
What "Correct MRR" Actually Requires
If you want a number you can trust, "just sum the dashboards" is not enough. Correct MRR across multiple products depends on four separate normalization steps.
1. Billing interval normalization
Annual revenue needs to be converted into a monthly equivalent. A $240 yearly plan contributes $20 MRR, not $240 in one month and nothing in the next eleven.
2. Processor-specific status filtering
Each system defines active revenue differently. Stripe has active, trialing, and past_due. Polar has its own subscription states. LemonSqueezy distinguishes subscription records from one-time orders. If you do not normalize status rules first, you are summing different business definitions.
3. Currency conversion
If one product bills in USD and another in EUR, the total only makes sense after conversion into a shared base currency. That sounds obvious, but many indie dashboards effectively compare unlike amounts.
4. Deduplication and revenue type separation
One-time revenue is useful, but it is not MRR. Migration edge cases can also double-count the same customer across processors. Correct tracking means separating recurring from non-recurring revenue before aggregation.
The Manual Code Approach
Here is the kind of normalization logic you end up writing if you decide to solve this yourself:
type Processor = "stripe" | "polar" | "lemonsqueezy";
type BillingInterval = "month" | "quarter" | "year";
type RevenueType = "subscription" | "one_time";
interface RevenueRecord {
id: string;
processor: Processor;
revenueType: RevenueType;
amountCents: number;
currency: string;
billingInterval: BillingInterval | null;
status: string;
}
const ACTIVE_STATUSES: Record<Processor, string[]> = {
stripe: ["active"],
polar: ["active"],
lemonsqueezy: ["active"],
};
function normalizeRecurringToMonthlyCents(record: RevenueRecord): number {
if (record.revenueType !== "subscription") return 0;
if (!ACTIVE_STATUSES[record.processor].includes(record.status)) return 0;
switch (record.billingInterval) {
case "month": return record.amountCents;
case "quarter": return Math.round(record.amountCents / 3);
case "year": return Math.round(record.amountCents / 12);
default: return 0;
}
}
function aggregateMrrInUsd(
records: RevenueRecord[],
fxRates: Record<string, number>,
): number {
return records.reduce((total, record) => {
const monthlyCents = normalizeRecurringToMonthlyCents(record);
if (monthlyCents === 0) return total;
const fxRate = record.currency === "USD"
? 1
: (fxRates[record.currency] ?? 1);
return total + Math.round(monthlyCents * fxRate);
}, 0);
}
This is not especially complex code. The problem is everything around it: calling multiple APIs, dealing with pagination, storing sync history, handling key rotation, and deciding how to classify edge cases consistently every time you recalculate.
The Aggregation Problem Across Processors
Even if your normalization code is correct, summing three processors still goes wrong when the source metrics are not comparable.
Stripe is optimized for subscription-heavy SaaS businesses. Polar often fits developer products and open-source monetization better. LemonSqueezy is strong for digital goods and smaller recurring offers. Those are different revenue environments. Each processor chooses its own labels, its own default reports, and its own assumptions about what should be visible first.
That means the simple "stripe number + polar number + lemonsqueezy number" approach is usually wrong. One source may be gross revenue. Another may be net of discounts. Another may include one-time orders in the same reporting surface. Without a normalization layer, aggregation is just arithmetic on mismatched definitions.
Automated Solution with Makerfolio
Makerfolio exists to make this problem boring.
Instead of maintaining your own aggregation script, you connect the processors you use and let Makerfolio calculate a single verified number with the same methodology every time.
What Makerfolio handles:
- Normalizing annual, quarterly, and monthly billing into one monthly revenue figure
- Aggregating Stripe and Polar into one verified dashboard today
- Preparing LemonSqueezy support so digital product revenue can live in the same profile
- Separating recurring MRR from one-time order revenue
- Converting currencies into a shared base for public reporting
- Storing snapshots over time so your public profile shows an actual revenue story, not a one-off screenshot
If you are already using Stripe or Polar, the integration is live today. If LemonSqueezy is part of your stack, you can join the early access waitlist now.
The Build-in-Public Bonus
There is another reason founders care about this beyond internal reporting: credibility.
Build-in-public updates work better when the audience trusts the number. A screenshot can be cherry-picked. A manually typed MRR total can be rounded, guessed, or interpreted differently month to month. A verified public profile is stronger because the number comes from an actual processor sync and follows the same calculation method every time.
That matters if you are sharing progress publicly, attracting customers, recruiting collaborators, or just trying to create a track record that other founders believe.
Conclusion
Tracking revenue from multiple products is not really a dashboard problem. It is a normalization problem. Once you sell through more than one processor, "just add the totals" stops working.
The durable solution is one system that understands Stripe, Polar, and LemonSqueezy as inputs to the same business, not three unrelated reporting silos.
If you want one verified revenue number across all of your products, start with the integrations you already use and see the full stack in one place: Explore Makerfolio integrations →