Skip to main content
Lago
Lago is a trademark of its respective owner, used here for identification and migration comparison only. No endorsement, partnership, or affiliation is implied.
Lago is an open-source metering and usage-based billing platform: you define billable metrics, attach them to plans, subscribe customers, then ingest usage events so Lago can aggregate consumption, enforce entitlements, and turn it all into invoices. It does billing extremely well — real-time metering, subscription management, entitlements, payment orchestration across Stripe/Adyen/GoCardless, and revenue analytics. But when the customer you are billing is really an agent that also holds cards, an inbox, a phone number, and a KYC’d identity, a Lago account is a disconnected vendor account:
  • Metering lives behind a Lago API key, and each customer is a Lago customer record — an identity that exists only inside Lago, unrelated to where the same agent’s cards, email, and permissions live.
  • Whether the agent’s plan actually lets it use a capability is an entitlement you configure in Lago and then re-enforce yourself at the call site — Lago meters and can flag limits, but the primitive access it is “entitling” is governed by a different system.
  • “Which plan is this agent on, what has it used this period, and is it allowed to do this right now?” is answered in Lago for usage, and in your own permission code for access — two sources of truth for one decision.
Naive’s customer-billing primitive gives you the same control-plane — define plans, subscribe tenants, meter usage, read a usage rollup — but the plan is the same object that governs the agent:
  • A Naive plan maps a key to an Account Kit (what the agent may do) plus per-primitive quotas (how much). Subscribing a tenant assigns that kit — so plan tier → permissions → quota is one call, not a Lago config plus separate entitlement code.
  • Usage is metered automatically on the primitive call itself — no event pipeline to build, secure, and de-duplicate. When a tenant exceeds a quota, the next call is refused with 429 rate_limited at execution time.
  • Every metered call lands in the same per-user activity log as that agent’s card spend, email sends, and KYC events — one accountability trail.
Tested against: the Lago Node client lago-javascript-client v1.48.0 (June 2026), against the Lago REST API (base https://api.getlago.com/api/v1, Authorization: Bearer, /billable_metrics, /plans, /customers, /subscriptions, /events, /customers/{id}/current_usage endpoints, docs snapshot July 2026), and the Naive Node SDK @usenaive-sdk/server against the Naive API (base https://api.usenaive.ai/v1, /v1/plans + /v1/users/:id/billing/* endpoints, docs snapshot July 2026).Version notes:
  • Scope differs. Lago bills any SaaS customer. Naive customer-billing bills a tenant user — the same handle (naive.forUser(id)) that reaches the agent’s cards, email, phone, vault, and KYC.
  • Naive does NOT own the charge. Naive owns plan → permissions → quota → usage. Your app keeps its Stripe charge (Naive plans carry an optional stripePriceId to link the two). If you rely on Lago for invoice generation, payment collection, coupons, wallets, or complex pricing, that stays with Lago/Stripe — see what doesn’t map yet.
  • Metering is built-in, not custom. Lago meters any code you define with any aggregation. Naive meters a fixed, documented set of primitives (seo, aeo, email) as a per-call count — you do not define custom billable metrics. See gaps.
  • Ingestion is automatic. Lago requires you to POST /events on every billable action. Naive records one usage event per successful metered primitive call — there is no ingestion endpoint to call.

Concept map

LagoNaiveNotes
Client(apiKey)new Naive({ apiKey }) then naive.forUser(id)Server-side credential in both cases
Customer (external_customer_id) via customers.createCustomertenant user via naive.users.create / naive.forUser(id)The Naive id also reaches cards, email, phone, KYC
Billable metric (billableMetrics.createBillableMetric, code + aggregation)Built-in metered primitives (seo, aeo, email)You don’t define custom metrics — see gaps
Plan (plans.createPlan, charges per metric)naive.plans.upsert({ key, name, accountKitId, quotas, period })POST /v1/plansA Naive plan = an Account Kit + per-primitive call quotas (caps, not priced charges)
— (entitlements configured separately)The plan’s accountKitId decides which primitives the agent may usePermission and plan are the same object — see gain #2
Subscription (subscriptions.createSubscription({ subscription: { external_customer_id, plan_code } }))naive.forUser(id).billing.setSubscription({ planKey })PUT /v1/users/:id/billing/subscriptionassignKit (default true) also applies the plan’s Account Kit in the same call
Usage event (events.createEvent({ event: { code, external_subscription_id, properties } }) on every action)Automatic — one usage event recorded per successful metered primitive callGain: no event pipeline to build, secure, or de-duplicate
Current usage (customers.findCustomerCurrentUsage(id, { external_subscription_id }))naive.forUser(id).billing.usage()GET /v1/users/:id/billing/usageReturns { plan, status, period, quotas, usage }
Overage / soft usage limitHard quota → 429 rate_limited on the next callEnforced at execution time, not reconciled on an invoice
stripe/adyen/gocardless payment provider on the customerYour app keeps its own Stripe charge; plan carries optional stripePriceIdNaive owns plan/quota/usage, not the charge
Invoices, wallets (prepaid credits), coupons, add-ons, credit notes, taxes, multi-currencyNot provided — see gaps
Revenue analytics (MRR, usage trends)Per-tenant usage rollup only (billing.usage())No cross-customer revenue analytics

Before / after: the core path

The path that matters for a usage-billed agent product is define a plan → subscribe a customer → meter their usage → enforce the quota. Here it is on both platforms.
import { Client } from "lago-javascript-client";

const lago = Client(process.env.LAGO_API_KEY!);

// 1. Define a billable metric + plan (usually done once, in dashboard or API).
await lago.billableMetrics.createBillableMetric({
  billable_metric: { name: "SEO calls", code: "seo", aggregation_type: "count_agg" },
});
await lago.plans.createPlan({
  plan: {
    name: "Pro",
    code: "pro",
    interval: "monthly",
    amount_cents: 9900,
    amount_currency: "USD",
    charges: [{ billable_metric_code: "seo", charge_model: "standard", properties: { amount: "0.01" } }],
  },
});

// 2. Create the customer + subscribe them to the plan.
await lago.customers.createCustomer({ customer: { external_id: "acme" } });
await lago.subscriptions.createSubscription({
  subscription: { external_customer_id: "acme", plan_code: "pro", external_id: "acme_pro" },
});

// 3. Meter usage — YOU must POST an event on every billable action.
await lago.events.createEvent({
  event: {
    transaction_id: crypto.randomUUID(),
    external_subscription_id: "acme_pro",
    code: "seo",
    timestamp: Math.floor(Date.now() / 1000),
    properties: {},
  },
});

// Whether "acme" may actually run an SEO call is enforced in YOUR code — Lago
// meters and can flag limits, but the primitive access lives in another system.
const usage = await lago.customers.findCustomerCurrentUsage("acme", {
  external_subscription_id: "acme_pro",
});
import { Naive } from "@usenaive-sdk/server";

const naive = new Naive({ apiKey: process.env.NAIVE_API_KEY! });

// 1. Define a plan = an Account Kit (permissions) + per-primitive quotas.
const proKit = await naive.accountKits.create({
  name: "Pro",
  primitives_config: { seo: { enabled: true }, aeo: { enabled: true }, email: { enabled: true } },
});
await naive.plans.upsert({
  key: "pro",
  name: "Pro",
  accountKitId: proKit.id,
  stripePriceId: "price_123",       // optional — link YOUR Stripe price
  quotas: { seo: 1000, aeo: 250 },  // calls per period
  period: "month",
});

// 2. Subscribe the tenant — assignKit (default true) applies the plan's kit,
//    so plan tier → permissions → quota is one call.
await naive.forUser("acme").billing.setSubscription({
  planKey: "pro",
  status: "active",
  stripeCustomerId: "cus_123",
  stripeSubscriptionId: "sub_123",
});

// 3. Metering is automatic. A successful SEO/AEO/email call records one usage
//    event; over quota → the next call is refused with 429 rate_limited.
const usage = await naive.forUser("acme").billing.usage();
// { plan: "pro", status: "active", period: "month",
//   quotas: { seo: 1000, aeo: 250 }, usage: { seo: 42 } }
The control-plane shape lines up closely. The real differences to plan for:
  • The plan carries permissions. A Naive plan references an accountKitId; subscribing assigns it. There is no separate “entitlement” object to keep in sync — the plan is the entitlement.
  • No event ingestion. Delete your events.createEvent(...) calls. Naive meters seo, aeo, and email on the primitive call itself; you never post usage.
  • Quotas are caps, not charges. Lago prices each metered unit; Naive quotas are per-period call limits that return 429 when exceeded. Pricing/collection stays in your Stripe.
  • Usage rollup is per tenant. billing.usage() returns the tenant’s quotas + consumption for the current period — the analogue of findCustomerCurrentUsage.

Minimal viable migration

The smallest swap that keeps a working, metered product running is plan → subscribe → let metering run.
1

Install the SDK and set your key

npm install @usenaive-sdk/server
Set NAIVE_API_KEY (a server-side key from the dashboard).
2

Recreate your plans as Account Kit + quotas

For each Lago plan, create an Account Kit enabling the primitives that tier may use, then naive.plans.upsert({ key, name, accountKitId, quotas, period }). Map your Lago plan code → Naive plan key, and your metered limits → quotas (per-primitive call caps). Carry your Stripe price across via the optional stripePriceId.
3

Reflect subscriptions from your Stripe webhook

Replace subscriptions.createSubscription(...) with naive.forUser(tenantId).billing.setSubscription({ planKey, status, stripeCustomerId, stripeSubscriptionId, currentPeriodEnd }). Drive it from the same Stripe webhook you already run. assignKit defaults to true, so the plan’s Account Kit is applied in the same call.
4

Delete the event ingestion path

Remove your events.createEvent(...) calls for the metered primitives. Naive records usage automatically on each successful seo, aeo, and email call — there is no ingestion endpoint.
5

Swap the usage read

Map customers.findCustomerCurrentUsage(id, ...)naive.forUser(id).billing.usage() for the current-period rollup + quotas.
6

Keep Stripe for the charge, then ship

Your app still owns invoicing/payment through Stripe (Naive doesn’t charge). At this point the metering + entitlement layer is off Lago. Everything below is upside.

Consolidate further once you’re on Naive

This is where the migration pays for itself. In Lago, a plan meters usage and (via entitlements) can describe access — but the primitives it entitles are governed elsewhere, so “billing plan” and “permission set” are two systems you keep in sync. On Naive, the plan is the Account Kit: the object that bills the customer is the object that governs their agent.
// Lago knows "acme is on Pro and used 42 SEO calls".
await lago.subscriptions.createSubscription({
  subscription: { external_customer_id: "acme", plan_code: "pro", external_id: "acme_pro" },
});
await lago.events.createEvent({
  event: { transaction_id: crypto.randomUUID(), external_subscription_id: "acme_pro", code: "seo", properties: {} },
});

// But whether acme's agent may issue a card, send email, or run SEO at all is
// enforced in a DIFFERENT system — Lago doesn't govern the primitive.
// One tenant user per customer; the same handle reaches every primitive.
const acme = await naive.users.create({ external_id: dbCustomer.id, email: dbCustomer.email });

// Subscribing to "pro" assigns the plan's Account Kit — so being on Pro IS
// what lets the agent use these primitives, capped by the plan's quotas.
await naive.forUser(acme.id).billing.setSubscription({ planKey: "pro", status: "active" });

// The SAME client owns this customer's card, inbox, and phone — one account,
// one budget, one audit trail, every capability governed by the plan's kit.
await naive.forUser(acme.id).cards.create({ spending_limit_cents: 25_000 });
await naive.forUser(acme.id).email.createInbox({ local_part: "ops" });

Gain #1 — one identity across billing and capability

  • With Lago, a customer is a billing record; the agent’s cards, email, phone, and permissions live in other systems. With Naive, naive.forUser(acme.id) is a single handle to the agent’s plan and primitives and permissions and audit trail — no separate customer record to reconcile.
  • Downgrading a customer’s plan re-assigns their Account Kit, which immediately changes what their agent may do — no second write to a permission system, no drift between “what they pay for” and “what they can touch”.

Gain #2 — execution-time permission enforcement

  • A plan’s accountKitId decides which primitives the agent may use, and quotas cap how much — both enforced at the moment of the call, not reconciled later on an invoice.
// A free tier that can do AEO but not SEO, and is capped tightly.
const freeKit = await naive.accountKits.create({
  name: "Free",
  primitives_config: { aeo: { enabled: true }, seo: { enabled: false } }, // seo → forbidden
});
await naive.plans.upsert({ key: "free", name: "Free", accountKitId: freeKit.id, quotas: { aeo: 25 } });
  • The agent’s code path stays the same for every tier. A tenant whose plan doesn’t enable seo is refused with forbidden; a tenant over its aeo quota is refused with 429 rate_limited — with no code change on your side:
{ "error": { "code": "rate_limited", "message": "Plan quota exceeded for 'aeo' (25/25 this period)", "used": 25, "limit": 25 } }
  • In Lago, exceeding a metered limit typically means an overage charge reconciled on the next invoice; on Naive the quota is a hard, execution-time gate. The two models are complementary — keep Stripe for the money, let Naive stop the call.

Gain #3 — unified accountability

  • Every metered call records the acting agent and lands in one per-user activity log — alongside that customer’s card spend, email sends, and KYC events, not in a separate Lago usage stream:
const { events } = await naive.forUser(acme.id).logs.query({ limit: 50 });
// "what did this customer's agent do, and against which plan?" — SEO/AEO/email
// usage, card spend, and email sends on one timeline.
  • That is the question that is hard to answer when usage lives in Lago, permissions live in your code, and the agent’s spend lives in Stripe. Under Naive it is a single query.

What does not map yet

A migration guide that hides gaps is worse than none. The metering + entitlement + quota layer maps cleanly and fuses with the agent’s permission model — but Lago is a full billing platform, and several of its capabilities have no equivalent on Naive’s customer-billing primitive today. Naive is explicit that it owns plan → permissions → quota → usage, not the charge. Check this list against your app before you commit.
Lago featureStatus on NaiveWorkaround
Invoicing + payment collection (Stripe/Adyen/GoCardless, dunning, retries)Not provided — Naive doesn’t generate invoices or move moneyKeep your Stripe charge; link it via the plan’s optional stripePriceId and drive setSubscription from your Stripe webhook
Custom billable metrics (any code, count/sum/max/unique/weighted aggregation, charge filters)Not provided — Naive meters a fixed set of primitives as a per-call countMeter custom app dimensions in your own store / keep Lago for those metrics
Metered primitive coverageDocumented metered set is seo, aeo, emailOther primitives are gated + budgeted, but not plan-quota-metered; track them via logs / billing
Priced usage / overage billingNot provided — quotas are hard caps (429), not priced chargesPrice usage in Stripe; use Naive quotas as the enforcement gate
Wallets / prepaid credits, coupons, add-ons, credit notesNot providedKeep in Lago/Stripe
Taxes + multi-currency, billing entitiesNot providedHandle in your Stripe/tax stack
Revenue analytics (MRR, usage trends, cross-customer)Per-tenant usage rollup only (billing.usage())Aggregate from your own store or keep Lago analytics
Batch event ingestion, historical event backfill/editingNot applicable — metering is automatic per call, not ingestedN/A (no ingestion endpoint)
Aggregation period flexibilityperiod is month or dayChoose the closest of the two
The biggest thing to weigh is scope, not method. Lago is an end-to-end billing platform: metering and invoicing and payment collection and pricing. Naive’s customer-billing primitive deliberately owns only plan → permissions → quota → usage — it does not issue invoices or collect payment, and it meters a fixed set of primitives (seo, aeo, email) as counts rather than arbitrary priced metrics. The right migration is to move the entitlement + metering + quota-enforcement layer onto Naive (so it is fused with the identity that governs the agent) while keeping Stripe for the actual charge. If Lago is load-bearing for invoicing, prepaid wallets, coupons, complex per-unit pricing, or custom billable metrics, keep that part of Lago/Stripe and adopt Naive for the permission-and-quota plane — don’t expect a full billing-stack replacement today.

Where to go next

  • Customer Billing primitive — plans → Account Kits, per-tenant subscriptions, metered usage, and quota enforcement
  • billing SDK sub-client — typed plans.upsert / setSubscription / usage
  • Account Kits — the permission object a plan assigns, enforced per user at execution time
  • Tenant users — the identity a subscription, usage, cards, email, and KYC all hang off
  • Billing & Credits — your own Naive subscription (distinct from billing your customers)
  • Logs — the unified per-user activity trail every metered call lands in