Customers and plans
Every event and every decision carries a customerId. That id is what AI cost is attributed to and what policies scope to. Where the customer behind it comes from depends on one thing: whether the project has connected a revenue source.
Without Stripe: your ids are the customers
Until a project connects Stripe, every customer id your app reports becomes a customer the first time MarginFuse sees it, whether in a track event or a guard decision. Nothing to configure, nothing to map. The customer is named by the id you sent, its AI cost accumulates from the first event, and it shows up in the customers list, the feature breakdown and the policy editor like any other.
What works from the first event:
- AI cost per customer, per feature and per model.
- Cost ceilings per customer per day or per calendar month, scoped to the whole project, one feature, or one customer.
- Feature budgets in a day or calendar-month window.
- Dry-run, decisions, alerts, and live protection for those policies.
Margin needs a revenue side. Without Stripe you supply it yourself, by declaring what you charge - see Declared plans below. Until you do either, revenue, gross profit and margin read as unavailable with the reason, never as zero, and the conditions that depend on them (billing-period windows, plan scopes and budgets, margin floors) are offered disabled with that reason. The server refuses them too.
Declared plans: margin without a revenue source
In Settings → Plans you declare the plans your product sells: a name, the price you charge, the currency, how often you bill, and a key your application can use. Then you put each customer on one, from the customer page or from your own code. MarginFuse derives revenue from that price across each cycle, and margin per customer, plan profitability and margin policies all start working.
Assigning plans from your code
identify tells MarginFuse who a customer is and which plan they are on. Call it when a customer signs in, signs up, or changes plan. It is safe to call every time: sending the plan they are already on changes nothing.
import { MarginFuse } from "marginfuse";
const mf = new MarginFuse({ apiKey: process.env.MARGINFUSE_KEY! });
// On sign-in, or whenever a customer's plan changes. Safe to call every time:
// sending the plan they are already on changes nothing.
const res = await mf.identify({
customerId: "user_8x2m91",
plan: "pro", // the key of a plan you declared in MarginFuse Settings
name: "Acme Studio",
metadata: { tier: "legacy" }, // labels segment policies can match on
});
// Unlike track(), this reports failure: a wrong plan is a wrong margin. It
// still never throws into your code.
if (!res.ok) console.warn("MarginFuse identify:", res.error);plan is the key from Settings → Plans, not a Stripe price id. Unlike track, this call reports failure rather than failing quietly, because a wrong plan is a wrong margin. It still never throws into your code: the result says it failed and why, and your onError handler is called.
- Backdating. Pass
periodStartwhen the customer has been paying since an earlier date, and MarginFuse counts the cycles since then. - Ending a plan. Pass
clearPlan: trueto take a customer off plans entirely. Revenue already accrued stays on record. - Shortcut.
trackandguardalso acceptplan, so a plan can ride along with usage instead of needing its own call. There it is a hint: a key that does not resolve is ignored rather than failing your event. - After Stripe. Once a revenue source bills a customer, that subscription is the truth.
identifyis then refused for that customer withprovider_subscription_present, and your code can keep calling it unchanged.
Details worth knowing:
- Cycles are anchored. A customer's cycle starts when you assign the plan, or on the earlier date you give. Every period is measured from that anchor, so a cycle starting on the 31st clamps to the 28th in February and returns to the 31st in March, without drifting.
- A free plan is a price of 0, not an absence of information. The customer then shows a real loss instead of an unknown, which is usually the point.
- Changing a plan takes effect now. The current cycle ends at that moment, the revenue accrued so far is prorated to it, and the new plan starts from there. Nothing is deleted; the old figure stays readable as what you asserted at the time.
- Plans are archived, not deleted, because recorded revenue points at them. Customers already on an archived plan keep accruing until you move them.
Connect Stripe later and it takes over: for each customer it bills, the declared subscription closes at that moment, the revenue accrued so far is kept, and collected figures replace declared ones. Customers Stripe does not bill keep their declared plan.
With Stripe: ids join revenue
Once Stripe is connected, MarginFuse imports its customers, plans and subscriptions and puts revenue next to cost. A reported id that is a Stripe customer id (cus_…) matches automatically. Any other id waits in the mapping queue under Settings, where an admin can map it to its Stripe customer, or keep it as an application customer when it simply is not in Stripe: a free tier, a trial that never converted, an internal account. Either way the cost that accumulated under the id moves with it and the affected reports recalculate.
Connecting Stripe later
Customers created from your ids stay when you connect Stripe. If an id you reported turns out to be the Stripe customer id, the sync promotes that customer in place: same row, same history, now with revenue. For other ids, use the remap action in the mapping queue to join them to their Stripe customer; an application customer left with no ids is archived, not deleted, so its history remains visible.
The rule is derived, not a setting: a project that has ever connected a revenue source treats unknown ids as mapping-needed; one that never has treats them as customers. There is no switch to get wrong.
NextProtectionDecide before the call runs, and act on the verdict.