Skip to main content

Analytics

The analytics resource on the SDK is how you read finished numbers back out of MarginFront. Revenue, cost, margin, billed, collected, MRR. Same math the dashboard runs, same shapes. All seven methods return plain TypeScript objects with typed fields. Money fields are number (not strings). If a margin can’t be calculated (because revenue is zero), marginPercent is null, never 0 or NaN. This page covers:
  1. analytics.revenue: revenue, cost, margin, and the breakdown of where revenue came from
  2. analytics.costBreakdown: cost sliced by agent, customer, signal, day, plan, and model
  3. analytics.agentEarned: activity-only revenue, before any fees are added on
  4. analytics.invoiceTotals: billed, collected, outstanding, overdue
  5. analytics.mrr: last complete calendar month’s billed total
  6. analytics.runRateMrr: trajectory if the last 30 days keep going
  7. analytics.committedMrr: contractual floor, regardless of usage
For the shape of every response type, see Types Reference.

Setup

import { MarginFrontClient } from "@marginfront/sdk";

const mf = new MarginFrontClient(process.env.MF_API_SECRET_KEY);
All seven methods are on mf.analytics. No separate import, no special setup.

1. analytics.revenue

What it does in plain English: gives you the whole revenue picture for a time window. How much you earned, how much it cost you, your margin, and where the revenue came from (per subscription, per pricing strategy). This is the number you’d show on an executive dashboard. It includes usage-based revenue plus recurring fees, seat fees, and one-time fees (prorated if the subscription only partly overlapped the window).

Signature

await mf.analytics.revenue(params: RevenueMetricsParams): Promise<RevenueMetrics>

Parameters

NameTypeRequiredDescription
startDatestringyesISO date, inclusive (e.g., "2026-04-01").
endDatestringyesISO date, inclusive.
customerIdstringnoFilter to one customer. UUID, not your external ID.
agentIdstringnoFilter to one agent. UUID.
signalIdstringnoFilter to one signal. UUID.
subscriptionIdstringnoFilter to one subscription. UUID.
Any combination of filters can be mixed. No filter means org-wide.

Response shape

interface RevenueMetrics {
  revenue: number;
  cost: number;
  margin: number;
  marginPercent: number | null;

  usageRevenue: number;
  recurringRevenue: number;
  seatRevenue: number;
  onetimeRevenue: number;

  eventCount: number;
  eventCountWithNullCost: number;

  bySubscription: SubscriptionRevenue[];
  byStrategy: StrategyRevenue[];
}

Example

// Org-wide revenue for April
const metrics = await mf.analytics.revenue({
  startDate: "2026-04-01",
  endDate: "2026-04-30",
});

console.log(`Revenue: $${metrics.revenue.toFixed(2)}`);
console.log(`Cost: $${metrics.cost.toFixed(2)}`);
console.log(`Margin: $${metrics.margin.toFixed(2)}`);

if (metrics.marginPercent !== null) {
  console.log(`Margin %: ${metrics.marginPercent.toFixed(1)}%`);
} else {
  console.log(`Margin %: not available (no revenue yet)`);
}

// Revenue for a single customer
const customerMetrics = await mf.analytics.revenue({
  startDate: "2026-04-01",
  endDate: "2026-04-30",
  customerId: "c4e8d1a2-1234-4567-8901-abcdef123456",
});

What this returns, in plain English

  • revenue total dollars earned in the window. Usage plus recurring plus seat plus one-time fees.
  • cost total service cost (the bill from OpenAI, Anthropic, Twilio, etc.) for the events in the window.
  • margin revenue minus cost.
  • marginPercent the margin as a percentage. Comes back as null (shown as a dash in dashboards) when revenue is zero, so you never see a 0% or NaN% that would be misleading.
  • usageRevenue / recurringRevenue / seatRevenue / onetimeRevenue the four ways revenue can show up, split out. They add up to revenue.
  • eventCount every event counted in the window.
  • eventCountWithNullCost events that are in the count but whose cost couldn’t be calculated yet (unknown model, no pricing map). A “needs attention” number. See Tracking Events: NEEDS_COST_BACKFILL.
  • bySubscription one row per active subscription in the window, each with its own revenue/cost/margin breakdown.
  • byStrategy one row per pricing strategy that contributed revenue, with the strategy’s charge type, pricing model, and signal id.

2. analytics.costBreakdown

What it does in plain English: shows you where your money is going. Total cost in the window, sliced six different ways: by agent, by customer, by signal, by day, by pricing plan, and by LLM model. Optional period-over-period comparison. This is the number behind a “cost explorer” dashboard. Find the expensive customer. Find the expensive model. Find the day your bill spiked.

Signature

await mf.analytics.costBreakdown(params: CostMetricsParams): Promise<CostMetrics>

Parameters

NameTypeRequiredDescription
startDatestringyesISO date.
endDatestringyesISO date.
customerIdstringnoFilter to one customer.
agentIdstringnoFilter to one agent.
signalIdstringnoFilter to one signal.
subscriptionIdstringnoFilter to one subscription.
includePriorWindowbooleannoWhen true, adds a prior field with same-length-previous-window totals.

Response shape

interface CostMetrics {
  cost: number;
  eventCount: number;
  eventCountWithNullCost: number;
  byAgent: CostByAgentRow[];
  byCustomer: CostByCustomerRow[];
  bySignal: CostBySignalRow[];
  byDay: CostByDayRow[];
  byPlan: CostByPlanRow[];
  byModel: CostByModelRow[];
  prior?: Omit<CostMetrics, "prior">;
}
Each breakdown row has the shape { <idField>: string | null, cost: number, eventCount: number }. See Types Reference for the exact row types.

Example

// Cost for April, with March comparison included
const cost = await mf.analytics.costBreakdown({
  startDate: "2026-04-01",
  endDate: "2026-04-30",
  includePriorWindow: true,
});

console.log(`Total cost: $${cost.cost.toFixed(2)}`);
console.log(`Events: ${cost.eventCount}`);

if (cost.eventCountWithNullCost > 0) {
  console.log(
    `Attention: ${cost.eventCountWithNullCost} events have no cost yet. ` +
      `Map their models in the dashboard.`,
  );
}

// Top 3 most expensive models
const topModels = cost.byModel.sort((a, b) => b.cost - a.cost).slice(0, 3);

for (const row of topModels) {
  console.log(`  ${row.model ?? "(unmapped)"}: $${row.cost.toFixed(2)}`);
}

// Trend vs prior window
if (cost.prior) {
  const delta = cost.cost - cost.prior.cost;
  const sign = delta >= 0 ? "+" : "";
  console.log(`Change vs prior window: ${sign}$${delta.toFixed(2)}`);
}

What this returns, in plain English

  • cost total service cost in the window.
  • eventCount every event in the window.
  • eventCountWithNullCost events whose cost couldn’t be calculated. Counted, not dropped. Shows up as a “needs attention” indicator.
  • byAgent / byCustomer / bySignal / byPlan / byModel cost broken out along each dimension. The id fields can be null when an event isn’t yet attached to that dimension (e.g., an orphan event with no pricing plan).
  • byDay cost per UTC calendar day, one row per day with activity.
  • prior only present when you passed includePriorWindow: true. Same shape as the outer object, but for the same-length window immediately before startDate. Lets you show trend arrows without a second call.

3. analytics.agentEarned

What it does in plain English: tells you how much revenue your agents produced from actual activity, before any fixed fees are added on top. Usage events multiplied by their pricing strategy rates, summed up. This is the earliest signal that something is working. It moves the moment an event fires, without waiting for an invoice to finalize. If you want to answer “are my agents doing billable work this week,” this is the number.

Signature

await mf.analytics.agentEarned(params: AgentEarnedParams): Promise<AgentEarned>

Parameters

NameTypeRequiredDescription
startDatestringyesISO date.
endDatestringyesISO date.
customerIdstringnoFilter to one customer.
agentIdstringnoFilter to one agent.
signalIdstringnoFilter to one signal.
subscriptionIdstringnoFilter to one subscription.

Response shape

interface AgentEarned {
  revenue: number;
  eventCount: number;
  bySubscription: AgentEarnedSubscriptionRow[];
  byStrategy: AgentEarnedStrategyRow[];
}

Example

// How much did my agents earn from activity this week?
const earned = await mf.analytics.agentEarned({
  startDate: "2026-04-20",
  endDate: "2026-04-26",
});

console.log(`Activity revenue: $${earned.revenue.toFixed(2)}`);
console.log(`From ${earned.eventCount} events`);

// Per-agent earnings
for (const row of earned.bySubscription) {
  console.log(
    `  Sub ${row.subscriptionId}: $${row.revenue.toFixed(2)} (${row.eventCount} events)`,
  );
}

What this returns, in plain English

  • revenue activity-only revenue in the window. Usage events × their pricing strategy rate. No recurring fees, no seat fees, no one-time fees.
  • eventCount events that produced revenue (events tied to a usage pricing strategy).
  • bySubscription per-subscription breakdown: how much revenue each sub produced and how many events did it.
  • byStrategy per-pricing-strategy breakdown: which specific rate earned what, and the total quantity that ran through it.

When to use this vs analytics.revenue

Use agentEarned when you care about activity in near-real-time. Use revenue when you want the full picture including fixed fees and prorations. They’ll give you different numbers for the same window, on purpose.

4. analytics.invoiceTotals

What it does in plain English: tells you what you actually invoiced and what your customers actually paid. Billed, collected, draft inventory, outstanding A/R, and a live overdue count. This is the finance-team view. It reads realized amounts off of invoices, not a formula. Non-draft invoices reconcile to the revenue formula, so you can trust these numbers directly.

Signature

await mf.analytics.invoiceTotals(params: InvoiceTotalsParams): Promise<InvoiceTotals>

Parameters

NameTypeRequiredDescription
startDatestringyesISO date.
endDatestringyesISO date.
customerIdstringnoFilter to one customer’s invoices.

Response shape

interface InvoiceTotals {
  billed: number;
  collected: number;
  draft: number;
  outstanding: number;
  overdueAmount: number;
  overdueCount: number;
  invoicesSentCount: number;
}

Example

const totals = await mf.analytics.invoiceTotals({
  startDate: "2026-04-01",
  endDate: "2026-04-30",
});

console.log(`Billed: $${totals.billed.toFixed(2)}`);
console.log(`Collected: $${totals.collected.toFixed(2)}`);
console.log(`Outstanding: $${totals.outstanding.toFixed(2)}`);

if (totals.overdueCount > 0) {
  console.log(
    `Overdue: ${totals.overdueCount} invoices, $${totals.overdueAmount.toFixed(2)} total`,
  );
}

What this returns, in plain English

  • billed total on invoices you issued in the window (status issued or overdue). Waiting on payment.
  • collected total on invoices you got paid for in the window (status paid).
  • draft total on invoices you generated but haven’t sent yet. Inventory, not revenue.
  • outstanding billed minus collected. Money you’ve asked for but haven’t received.
  • overdueAmount total on invoices currently past due. Age-based, not window-based. Current state right now.
  • overdueCount how many invoices are currently past due.
  • invoicesSentCount how many non-draft invoices were dated in the window.

5. analytics.mrr

What it does in plain English: tells you how much you actually billed last complete calendar month. Monthly recurring revenue, based on invoices that went out. This is the “book MRR” number. Stable across the month (doesn’t change until the month rolls over), based on realized billing. If you want forward-looking MRR, use runRateMrr or committedMrr below.

Signature

await mf.analytics.mrr(params?: MrrParams): Promise<Mrr>

Parameters

All optional.
NameTypeRequiredDescription
customerIdstringnoFilter to one customer.
subscriptionIdstringnoFilter to one subscription.
No date parameters. The window is fixed: first of last calendar month to first of this month.

Response shape

interface Mrr {
  mrr: number;
  arr: number;
}

Example

const { mrr, arr } = await mf.analytics.mrr();

console.log(`MRR: $${mrr.toFixed(2)}`);
console.log(`ARR: $${arr.toFixed(2)}`);

// Per-customer MRR
const customerMrr = await mf.analytics.mrr({
  customerId: "c4e8d1a2-1234-4567-8901-abcdef123456",
});

What this returns, in plain English

  • mrr total billed across all invoices dated in the previous calendar month.
  • arr annualized: mrr × 12. A convenience so you don’t multiply in the UI.

6. analytics.runRateMrr

What it does in plain English: tells you what your MRR would be if the last 30 days of activity kept going at the same pace. Recurring fees, seat fees, and actual usage rolled forward. This is the “current run rate” number. Useful when usage moves fast and last month’s MRR is already stale.

Signature

await mf.analytics.runRateMrr(params?: MrrParams): Promise<RunRateMrrResult>

Parameters

Same as analytics.mrr: optional customerId and subscriptionId. No date window.

Response shape

interface RunRateMrrResult {
  mrr: number;
  breakdown: RunRateMrrBreakdownRow[];
}

interface RunRateMrrBreakdownRow {
  subscriptionId: string;
  customerId: string;
  agentId: string;
  planId: string;
  recurring: number;
  seatBased: number;
  usage: number;
  total: number;
}

Example

const { mrr, breakdown } = await mf.analytics.runRateMrr();

console.log(`Run-rate MRR: $${mrr.toFixed(2)}`);

// Top 3 contributing subscriptions
const top = breakdown.sort((a, b) => b.total - a.total).slice(0, 3);
for (const row of top) {
  console.log(
    `  Sub ${row.subscriptionId}: $${row.total.toFixed(2)} ` +
      `(recurring $${row.recurring.toFixed(2)}, ` +
      `seats $${row.seatBased.toFixed(2)}, ` +
      `usage $${row.usage.toFixed(2)})`,
  );
}

What this returns, in plain English

  • mrr projected monthly revenue if the last 30 days keep up this pace.
  • breakdown one row per active subscription with its contribution split into recurring, seat, and usage parts. The three add up to total, and all the totals add up to mrr.

7. analytics.committedMrr

What it does in plain English: the floor. Tells you the monthly revenue your contracts guarantee, regardless of whether customers actually use the product. Recurring fees, seat fees, and usage minimums only. Use this to answer “what’s the least we’ll bill next month?” By definition, committed MRR is always less than or equal to run-rate MRR (because run-rate counts actual usage, committed only counts minimums).

Signature

await mf.analytics.committedMrr(params?: MrrParams): Promise<CommittedMrrResult>

Parameters

Same as analytics.mrr and analytics.runRateMrr: optional customerId and subscriptionId.

Response shape

interface CommittedMrrResult {
  mrr: number;
  breakdown: CommittedMrrBreakdownRow[];
}

interface CommittedMrrBreakdownRow {
  subscriptionId: string;
  customerId: string;
  agentId: string;
  planId: string;
  recurring: number;
  seatBased: number;
  usage: number; // minimum commitment × rate, not actual usage
  total: number;
}

Example

const { mrr: committed } = await mf.analytics.committedMrr();
const { mrr: runRate } = await mf.analytics.runRateMrr();

console.log(`Committed MRR (floor): $${committed.toFixed(2)}`);
console.log(`Run-rate MRR (actual pace): $${runRate.toFixed(2)}`);

const gap = runRate - committed;
console.log(`Usage upside: $${gap.toFixed(2)}`);

What this returns, in plain English

  • mrr the monthly revenue your contracts guarantee. Recurring fees always apply. Seat fees use the booked seat count. Usage uses the minimum commitment on each pricing strategy (zero when there’s no minimum set).
  • breakdown same shape as runRateMrr’s breakdown, but the usage column uses the minimum-commitment floor instead of actual events.

Field types at a glance

All dollar amounts are number, not string. Values come back at full precision. Round at render time, not in storage:
const metrics = await mf.analytics.revenue({ ... });

// Right: one round at the UI boundary
display(metrics.revenue.toFixed(2));

// Wrong: stored rounded numbers lose precision for downstream math
store(Number(metrics.revenue.toFixed(2)));
marginPercent is number | null. A null means “no revenue yet, so margin can’t be computed.” Render it as a dash, not as 0%. See Types Reference for the full interface definitions.

When to call which method

QuestionMethod
What did we earn this month?analytics.revenue
Where is our money going?analytics.costBreakdown
How much activity in the last 24 hours?analytics.agentEarned
What’s in A/R?analytics.invoiceTotals
What was last month’s MRR (stable reporting number)?analytics.mrr
What’s our current run rate?analytics.runRateMrr
What’s our contractual floor?analytics.committedMrr

Next steps