Guides#mrr#stripe#polar

How to Track Revenue Across Multiple Products (Stripe, Polar & LemonSqueezy)

Most indie hackers running multiple products waste hours reconciling Stripe dashboards, Polar accounts, and LemonSqueezy reports. Here's how to get one number that's actually correct.

Makerfolio··8 min read

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:

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 →

#mrr#stripe#polar#lemon-squeezy#revenue-tracking#indie-hacker
← All posts

Track what you just learned in Makerfolio.

Connect Stripe or Polar and get verified metrics on your public builder profile.

Start for free →
Published: March 24, 2026