# Limitr: The Usage Runtime for AI Products > Limitr is an embedded runtime that enforces what every user and agent can do, how much they get, and what it costs them, giving you control over cost-to-serve and revenue capture. This file contains all documentation content in a single document following the llmstxt.org standard. ## Concepts The terms used across the project and docs. All of the vocabulary and concepts are listed most important to least. Reference pages link back here rather than re-explain. --- ## Policy The configuration document that defines a product's usage monetization, control, and analytics strategies in their entirety. Fully managed in Limitr Cloud, or written with JSON, YAML, TOML, or [Stof](https://stof.dev), following the [spec](./reference/policy). Contains credit definitions (tokens, GPU seconds, abstract credits, or anything countable), overhead costs, your prices, plans, credit exchange rates, included limits, topups, entitlements — everything defined outside of customer state. --- ## Credit The definition of a descrete unit of value that can be delivered or used within your product. This can be a vendor unit that your product consumes, a unit of value you deliver to your customers, or a single line item/purchase. :::note Credits define the measurement units kept within meters and the units returned within SDK calls: int, float, seconds, MiB, any Stof unit. Because we have native types for things like seconds in the runtime, you can set limits in days for example, then meter in a mix of ms, s, hrs, and have all value, remaining, etc. SDK calls return numbers that always are in seconds. ::: **Discrete Credit** — Tied to a specific resource with real cost attached, either an `overhead_cost` or `price`, or both. - AI input token that costs you $0.000004 per token to use - GPU second that you charge your users for - A megabyte of storage that you want to keep track of - One SMS message that has a cost to send - Currency definition & exchange rates (e.g. Euro, Kroner, USD, British Pound) - Note: Limitr Cloud comes with currency exchange, you'll need a bit of Stof know-how if you want real-time exchange lookups with the open-source engine alone **Abstract Credit** — A credit definition that only has meaning in your product(s), may or may not have costs attached. - A flat monthly plan subscription cost - Single seat in your platform - User-facing credit that customers have a balance of, or purchase quantities of - Single SKU that you charge for :::tip[Need to know] The Credit is the only concept in Limitr that relates to money — any unit of value with a price, cost, or unit that matters to you or your customer (real or "fake") is a credit. The Exchange Table defines transforms between credits (can be multi-step), so that you can cap, limit, measure, analyize, etc. usage in the credit/units of your choice, always. ::: ### The Rune There's one abstract credit that is **always** defined, called the ***Rune*** or `rune` in each policy. It's main purpose is to give us a common base unit that is detached from real currency, it enables global credit exchanges, and has other benefits as well. When you set a `price` or `overhead_cost` or `tiers` on a credit, the defined value (float) is in runes, **not USD**. By default, a single Rune is defined as **$1 USD**, so technically, the units can be used interchangeably, and you can think of prices as USD in the config. :::tip[founder recommendation] I *highly recommend* you keep this setup, even if you use euros or something in your stack — I promise, things will work out better for you if you do. But if you must, you can change this definition and everything will still work fine. ::: :::note If you're using Limitr Cloud, this complexity is abstracted away for the most part. When you set prices in USD, we exchange it to runes for the policy config, and then back to USD for the UI. ::: --- ## Exchange An object within the policy that defines credit relationships and conversions. All values are expressed relative to the **rune**, where 1 rune = 1 USD by default. The exchange table is what allows a pool of abstract credits to drain across multiple discrete entitlements, and what enables margin calculation across any combination of resources. :::note Any credit with a `price` or `tiers` defined (in runes) automatically has an exchange rate at that price. Yes, tiered pricing is taken into account during the exchange as well — flat, tiered, volume, or stairstep pricing. - flat — a single price per unit, applied uniformly regardless of how much is consumed. - tiered - like tax brackets, each band of consumption has its own per-unit price, and you pay each band's rate only for the units consumed within that band. - volume - your total consumption determins a single per-unit rate, applied retroactively to all units consumed. - stairstep - a flat fee per band, where your total consumption determines which band you're in and you pay that band's fixed price regardless of where within the band you land. ::: ### Grant Strategy The rule that governs which grant is consumed first when multiple customer credit grants are eligible. Grants defined in the credit of consumption are always used first, regardless of strategy. :::tip[founder recommendation] Only useful if you have multiple abstract, user-facing credits — not desireable, but sometimes necessary (migrations, acquisitions, merging customer objects, honoring old topup purchases, etc.). Don't worry about this setting until you encounter a behavior that needs changing. The default "expires_first" strategy is most certainly what you'll want. ::: - `expires_first` — (default) prefer to use the credit grant that expires first - `cheapest_first` — choose the lowest rune value per unit (convert to credits that are cheapest, uses more) - `valuable_first` — choose the highest rune value per unit (convert to credits that are most expensive, uses less) --- ## Plan A named tier bundling entitlements for customers — `free`, `starter`, `growth`, `enterprise`, or whatever your product defines. Plans specify which credits customers on that tier can consume, at what limits, and which topups are available or included automatically. Every customer is on exactly one plan at a time. :::note A plan can be for your paying users, like `free`, `starter`, etc. But it could also be internal-facing to control your own usage: `workspace plan`, `agent plan`, `high-availability plan`, etc. Limitr does not restrict the types of "customers", so a `user` customer object with limits is just as valid as an `agent` customer object that owns usage state for one of your internal AI agents. And remember, you can always stack entitlements — they work together (e.g. user gets 1000 tokens per hour, agent gets 500 per hour, both must `allow(...)` -> `true` for the vendor call to occur). ::: --- ## Entitlement A specific resource a customer on a plan is allowed to use. Could map to a feature (e.g. ai-chat), a vendor line-item (e.g. sonnet-input-tokens), or anything you'd like to gate or control. Entitlements live inside plans and are identified by name (`chat_input`, `seats`, `pdf_export`). - **Without a limit** — acts as a boolean flag. Present means allowed; absent means denied. - **With a limit** — metered. Consumption is tracked and enforced against the limit - optionally billed for overage if overage allowed. :::note Entitlements can be scoped to specific types of customers. Each customer has a type (`user`, `org`, `agent`, `workspace`, etc.), and if an entitlement has a scope, it can only be applied to that type of customer. Customers reference each other, so Limitr will auto-resolve customer references to the required scope of an entitlement when used. For example, an org might have a limit on seats — users in that org don't, but the `seats` entitlement is scoped to `org`, so even when a user ID is passed into `allow(userId, 'seats', 1)`, Limitr resolves the `org` customer instead (shared meters). ::: ### Limit The constraint on an entitlement that allows/represents consumption. References the [credit](./concepts#credit) being metered by name (e.g. `seats`, `sonnet-input-token`), defines a limit value (units of the credit), and how enforcement behaves when the limit is reached. | Mode | Behavior | |---|---| | `hard` | Blocks at the limit (default post-op value comparison). No overage allowed (except when covered by a grant). Fires `meter-limit` events when hit. | | `soft` | Allows consumption past the limit. Fires `meter-overage` events for all overage that occurs. | | `observe` | No enforcement. Meters indefinitely. Useful for visibility & analytics without blocking (e.g. `logins`, `runs`). | Limits can reset on a schedule — either a fixed duration (`reset_inc: 30days`) or a calendar boundary (`reset_sch: 'monthly:1'`). They can also carry a **governor** — a token bucket rate ceiling below the hard limit that shapes consumption proactively rather than suddenly cutting customers off. Limits can be overridden per-customer without changing the plan. --- ## Customer Any entity that is controlled by or consumes entitlements — a user, an organization, a workspace, an agent. Customers are identified by a primary ID, can carry alternative IDs (e.g. a Stripe customer ID), and can reference other customers (e.g. a user referencing its org for a shared limit or meter). Each customer carries their own caps, meters, overrides, and grants. ### Meter The per-customer, per-entitlement consumption counter. Updated automatically on every `allow()`, `increment()`, or `decrement()` call. Stores the credit ID, current value, and a short history of recent consumption events. From the history, Limitr derives a real-time consumption **rate** and **projected exhaustion** timestamp — how long until the customer runs out at their current pace. When a governor is configured, the meter also tracks the token bucket state used for rate enforcement. Meters reset when their entitlement limit's reset schedule fires (can be overridden per customer). ### Grant A credit balance on a specific customer, created when a topup is applied (or included, potentially on a schedule). When a `soft`-limit entitlement goes into overage, Limitr draws from the customer's eligible grants before any overage is emitted or recorded. Grants track their `starting_value`, current `value`, reset behavior, expiry, and which topup created them (if at all). ### Spend Cap A spend cap is a high-level tool tied to a customer that contains it's own state and sits outside of the policy definition. It's purpose is to either observe or control spend (in any credit or exchange units), either temporarily, permamently, or on a schedule. Even if a limit is `soft` and allows overage, a spend cap can block an `allow(...)` or `check(...)` to protect or enforce spend tied to this customer. Can be applied in many situations, including but not limited to: - Self-serve USD or credit spend caps (user can set these for themselves) - Internal overhead spend caps (works on `overhead_cost`, too) — e.g. cap an AI agent to $15 in AI per month across all vendors - Measure spend & margin over a single pipeline run — involving many vendors, prices, etc. :::note Spend caps work, even with user-facing credits, grants, included usage limits, etc. All of these edge cases are handled, making spend caps one of the more powerful tools in the project. ::: --- ## Topup A credit package that can be applied to a customer — purchased explicitly or included automatically in a plan. When applied, a topup creates a [grant](#grant) on the customer. Topups can carry a price, reset on a duration or calendar schedule, support rollover, and expire after a configurable period. --- ## Notification A policy-defined rule that fires when a specific event condition is met. Written in Stof. Defines a `matches(type, event)` function to filter events, and a `fire` function to handle them. Notifications happen in real-time, and can route-automatically to Slack, email, etc. Locally, they fire in-process, allowing you to attach or inject your own handlers where desired. :::note If you're using Limitr Cloud, real-time notifications are configured in the UI, routed to your team of choice the millisecond thresholds are crossed/matched, and can be used throughout your organization for usage-tied insights and responsibilities. ::: --- ## Capability A named, callable unit of policy logic with defined input/output parameters. Used to expose policy-aware functions as dynamic tool definitions — for example, as Claude MCP tool definitions in an agent pipeline. Capabilities are an advanced concept, involving some Stof know-how. However, a powerful mechanism for policy-aware contexts and custom tooling. :::note The Limitr team is excited to help any Limitr Cloud customers with premium support create capabilities and tooling. Reach out on Slack whenever you need. ::: As such, they may be shown in cookbooks or guides, but not explicitly covered outside of this concepts page. Here's a brief example: export const capabilityTS = ` /** Setup your policy per usual. */ const policy = await Limitr.new(); /** TypeScript function that I want to make into a policy-aware capability. */ async function doTheThing(url: string): Promise { return url.length; } policy.doc.lib('Host', 'do_the_thing', doTheThing); // expose to the Stof wasm sandbox /** Add Stof capability/tooling inside the policy to do_the_thing - can add usage, enforcement, etc. */ await policy.setCapabilities(\` do_it: { parameters: [{ name: 'url', description: 'A URL string.' }] #[run] fn execute() { const res = await Host.do_the_thing(self.input.url ?? 'dne'); self.set_result(res ?? 0); } }\`); /** Now we have "do_it" as a policy capability, accessible to anything that has the policy! */ console.log(await policy.runCapability('do_it', { url: 'https://doin_it.com' })); // 19 /** Capabilities generate tools automatically and can be used out of the box with Claude. */ console.log(await policy.claudeTools()); /** Pass tool use blocks right into the policy to use with capabilities. */ const toolUse = { type: 'tool_use', id: 'toolu_21345', name: 'do_it', input: { 'url': 'http://z.ai' } }; const result = await policy.claudeToolUse(toolUse); console.log(result); `; export const capabilityStof = ` // Can be defined externally like the TS version Host: { async fn do_the_thing(url: str) -> int { url.len() } } // Can be a normal obj, a string from a remote src, etc. - Stof is versitile Capability do_it: { name: 'do_it' parameters: [{ name: 'url', description: 'A URL string.' }] #[run] fn execute() { const res = await root.Host.do_the_thing(self.input.url ?? 'dne'); self.set_result(res ?? 0); } }; #[main] fn main() { self.policy.set_capability(self.do_it); pln(self.policy.run_capability('do_it', { 'url': 'https://doin_it.com' })); pln('\\n' + .api.claude_tools()); const tool_use = new { type: 'tool_use', id: 'toolu_21345', name: 'do_it', input: new { 'url': 'http://z.ai' } }; const res = self.policy.claude_tool_use(tool_use, self); pln('\\n' + stringify('json', res)); }`; export const capExample = { title: 'capability', temp: true, showPolicy: false, policyFormat: 'stof', policy: "policy: {}", stof: capabilityStof, typescript: capabilityTS, }; --- ## SDK Quick Reference Index of every method on the Limitr class, and where to find more information. A complete index of every method on the `Limitr` class. Each entry links to the page where the method is fully documented with parameters, return types, and examples. The SDK surface maps directly to policy concepts — if you know what you're trying to do, the concept page is usually the right place to start. This page is for when you know the method name and want to find it fast. --- ## Initialization \{#initialization} | Method | Description | |---|---| | `Limitr.new(policy, format, validate?)` | Async constructor. Loads a policy from a string, object, or binary. Validates by default. → [Policy Reference](./reference/policy) | | `Limitr.cloud(options)` | Connect to Limitr Cloud. Returns a fully initialized `Limitr` instance backed by the Cloud WebSocket. → [Cloud Quick Start](../guides/cloud/quickstart) | | `policy.valid()` | Returns `[boolean, string]` — whether the policy is valid and the current error if not. → [Policy Reference](./reference/policy#validation) | | `policy.version()` | Returns the Limitr engine version string. | | `policy.close()` | Flush pending state and close the Cloud WebSocket connection. → [Cloud Quick Start](../guides/cloud/quickstart) | | `policy.doc` | The underlying `StofDoc`. Use for direct serialization: `policy.doc.stringify('yaml')`, `policy.doc.record()`. → [Policy Reference](./reference/policy) | --- ## Enforcement \{#enforcement} The methods your application calls on every request. These are on the hot path. | Method | Description | |---|---| | `policy.allow(customer, entitlement, value?, event?)` | Enforce and meter in one operation. Returns `false` if the limit blocks the request. Fires `meter-limit` or `meter-overage` events. → [Enforcement](./reference/enforcement#allow) | | `policy.check(customer, entitlement, value?)` | Read-only pre-authorization. Returns whether `allow()` would succeed without consuming quota. → [Enforcement](./reference/enforcement#check) | | `policy.increment(customer, entitlement, event?)` | Consume one `limit.increment` unit. Use for countable resources: seats, API calls, documents. → [Enforcement](./reference/enforcement#increment) | | `policy.decrement(customer, entitlement, event?)` | Release one `limit.increment` unit. Use when a resource is returned: seat removed, file deleted. → [Enforcement](./reference/enforcement#decrement) | | `policy.set(customer, entitlement, value, event?)` | Set the meter to an absolute value. Use for state-based resources where you know the total, not the delta. → [Enforcement](./reference/enforcement#set) | | `policy.checkIncrement(customer, entitlement)` | Read-only. Whether `increment()` would succeed. | | `policy.checkDecrement(customer, entitlement)` | Read-only. Whether `decrement()` would succeed. | --- ## Reading state \{#reading-state} | Method | Description | |---|---| | `policy.value(customer, entitlement, percent?, grants?)` | Current meter value. Pass `percent: true` for a 0–100 percentage of the limit. → [Enforcement](./reference/enforcement#value) | | `policy.remaining(customer, entitlement, percent?, grants?)` | Remaining balance (`limit - value`). Includes grant balances by default. → [Enforcement](./reference/enforcement#remaining) | | `policy.allowance(customer, entitlement, grants?)` | Available allowance right now — minimum of governor token balance and remaining period balance. Use to pre-size operations. → [Enforcement](./reference/enforcement#allowance) | | `policy.projectedExhaustion(customer, entitlement, smoothed?, grants?)` | Estimated ms until entitlement is exhausted at current rate. Pass `smoothed: true` for EWMA rate. → [Enforcement](./reference/enforcement#projectedexhaustion) | | `policy.rate(customer, entitlement)` | Instantaneous consumption rate in units/ms. → [Enforcement](./reference/enforcement#rate) | | `policy.acceleration(customer, entitlement)` | Rate of change between the two most recent intervals. → [Enforcement](./reference/enforcement#acceleration) | | `policy.resets(customer, entitlement)` | Unix timestamp (ms) when the meter will next reset. → [Enforcement](./reference/enforcement#resets) | | `policy.limit(customer, entitlement, grants?)` | The enforced limit value, including any customer override. → [Enforcement](./reference/enforcement#limit) | | `policy.cost(id, entitlement)` | The rune cost of one `increment()` on this entitlement. → [Enforcement](./reference/enforcement#cost) | --- ## Events \{#events} | Method | Description | |---|---| | `policy.addHandler(name, handler)` | Register a named event handler. Receives all `meter-changed`, `meter-limit`, and `meter-overage` events, plus any custom notification events. → [Enforcement](./reference/enforcement#addhandler) | | `policy.removeHandler(name)` | Remove a handler by name. → [Enforcement](./reference/enforcement#removehandler) | | `policy.clearHandlers()` | Remove all registered handlers. → [Enforcement](./reference/enforcement#clearhandlers) | --- ## Customers \{#customers} | Method | Description | |---|---| | `policy.createCustomer(id, plan?, type?, label?, refs?, alts?, metadata?)` | Create a new customer. Fires `customer-set`. → [Customers](./reference/customers#createcustomer) | | `policy.ensureCustomer(id, plan?, type?, label?, refs?, alts?, metadata?)` | Create a customer only if they don't already exist. Returns `true` if created. → [Customers](./reference/customers#ensurecustomer) | | `policy.customer(id)` | Full customer object: plan, meters, grants, overrides, metadata. Accepts primary ID or any alt ID. → [Customers](./reference/customers#customer) | | `policy.customers()` | All customers as a single record. The complete state snapshot for persistence. → [Customers](./reference/customers#customers) | | `policy.customerMetadata(id)` | The metadata object for a customer. → [Customers](./reference/customers#customermetadata) | | `policy.customerRefs(id)` | The list of referenced customer IDs. → [Customers](./reference/customers#customerrefs) | | `policy.setCustomerPlan(id, planId, overwrite_meters?)` | Change a customer's plan. Resets meters by default. → [Customers](./reference/customers#setcustomerplan) | | `policy.setCustomer(customer, event?)` | Set a customer by record object or JSON string. → [Customers](./reference/customers#setcustomer) | | `policy.removeCustomer(id)` | Remove a customer from the local policy. Fires `customer-removed`. → [Customers](./reference/customers#removecustomer) | | `policy.addAltID(existing, alt, event?)` | Add an alternative ID to an existing customer. → [Customers](./reference/customers#addaltid) | | `policy.removeAltID(alt, event?)` | Remove an alternative ID. → [Customers](./reference/customers#removealtid) | | `policy.loadCustomers(customers)` | Load many customer records at once. For restoring persisted state. → [Customers](./reference/customers) | | `policy.ensureCustomerPlanQuantity(id)` | Increment the plan subscription entitlement if the meter is below 1. Use to trigger subscription billing. → [Customers](./reference/customers#subscription) | --- ## Entitlements & overrides \{#entitlements} | Method | Description | |---|---| | `policy.entitlement(id, entitlement)` | The entitlement record for a plan or customer, with the resolved limit applied. → [Entitlements](./reference/entitlements#entitlement) | | `policy.createCustomerOverride(id, entitlement, value?, expires_on?, credit?, mode?, increment?, resets?, reset_inc?, reset_sch?)` | Replace a customer's limit for a specific entitlement. `reset_inc` and `reset_sch` are mutually exclusive. Returns override node ID. → [Entitlements](./reference/entitlements#createcustomeroverride) | | `policy.removeCustomerOverride(id, entitlement)` | Remove a customer override. Customer reverts to plan limit. → [Entitlements](./reference/entitlements#removecustomeroverride) | --- ## Credits \{#credits} | Method | Description | |---|---| | `policy.credit(id)` | The credit record for a given credit ID. → [Credits](./reference/credits#credit) | | `policy.creditFor(id, entitlement)` | The credit backing a specific entitlement, resolved from a plan or customer ID. → [Credits](./reference/credits#creditfor) | | `policy.creditExchange(inCredit, outCredit, value?)` | Convert a value from one credit to another via the exchange table. Returns `null` if no path exists. → [Exchange](./reference/exchange#creditexchange) | | `policy.remainingCredit(customerId, credit)` | Total remaining balance of a credit across all of a customer's grants, after exchange conversion. → [Credits](./reference/credits#remainingcredit) | --- ## Plans \{#plans} | Method | Description | |---|---| | `policy.plan(id, def?)` | Plan record for a plan ID or customer ID. Falls back to the default plan if `def` is `true`. → [Plans](./reference/plans#plan) | | `policy.defaultPlan()` | The plan marked `default: true`, if any. → [Plans](./reference/plans#defaultplan) | | `policy.setPlan(id, planStof)` | Add or replace a plan by ID. Fires `plan-set`. → [Plans](./reference/plans#setplan) | | `policy.deletePlan(id)` | Remove a plan by ID. Fires `plan-removed`. → [Plans](./reference/plans#deleteplan) | | `policy.planPeriod(id)` | Period string for a plan: `'monthly'`, `'yearly'`, `'weekly'`, `'daily'`. → [Plans](./reference/plans#planperiod) | | `policy.planTrialPeriod(id)` | Trial period in milliseconds, or `null`. → [Plans](./reference/plans#plantrialperiod) | | `policy.planSubEntitlementName(id)` | The subscription entitlement name for a plan. → [Plans](./reference/plans#plansubentitlementname) | --- ## Topups & grants \{#topups} | Method | Description | |---|---| | `policy.applyCustomerTopup(customerId, topupId)` | Apply a plan topup to a customer, creating a grant. → [Topups & Grants](./reference/topups#applycustomertopup) | | `policy.ensureCustomerIncludedTopups(id, event?)` | Sync included topup grants with the customer's current plan. Call after plan changes. → [Topups & Grants](./reference/topups#ensurecustomerincludedtopups) | --- ## Margin \{#margin} | Method | Description | |---|---| | `policy.customerMarginSnapshot(customerId)` | Live margin breakdown for a customer: revenue, cost, and margin per entitlement. → [Margin](./reference/margin#customermargin) | | `policy.marginSnapshot(planId, entitlements)` | Project margin for a plan given hypothetical entitlement values. For pricing decisions and plan design. → [Margin](./reference/margin#marginsnapshot) | --- ## Notifications \{#notifications} | Method | Description | |---|---| | `policy.setNotifications(contents, format?)` | Load notification definitions into the policy at runtime. → [Notifications](./reference/notifications#runtime) | --- ## Cloud \{#cloud} | Method | Description | |---|---| | `policy.addVoucher(voucher, handler?)` | Authenticate a Limitr voucher (proxy customer) for Limitr Network. → [Cloud Quick Start](../guides/cloud/quickstart) | | `policy.docCall(path, ...args)` | Call a Stof function in the policy document directly. For advanced use cases. | --- ## Quickstart Install the SDK, write a policy, make your first call. About five minutes. Limitr is an embedded engine. This means all of the action happens directly within your app, just as if it were your own code. ## Install Execution happens as WebAssembly. Each SDK is a thin wrapper around it. ```bash npm i @formata/limitr ``` ## Policy-as-Document A **Limitr Policy** is a config document that defines your credit definitions, plan configurations, usage limits, prices, etc. Here, it's defined as JSON, but YAML, TOML, or Stof also work fine. :::note If you're using Limitr Cloud, policies are fully managed, versioned, and live-editable from the app — you don't need to write them manually, unless you'd like a static/offline fallback or test policy. ::: ```json "policy": { "credits": { "seat": { "price": { "amount": 19.99 } }, "token": { "price": { "amount": 0.00004 }, "overhead": 0.000003 } }, "plans": { "starter": { "label": "Starter Plan", "entitlements": { "seats": { "limit": { "credit": "seat", "value": 3 } }, "ai-chat": { "limit": { "credit": "token", "value": 1000, "mode": "soft", "resets": true, "reset_inc": "1hr" } } } } } } ``` This policy has one `starter` plan with two entitlements: `seats` and `ai-chat`. Customers on the starter plan are entitled to 3 seats max and 1000 included tokens per hour, then are charged for every token over that at the defined price. export const policyJson = ` "policy": { "credits": { "seat": { "price": { "amount": 19.99 } }, "token": { "price": { "amount": 0.00004 }, "overhead": 0.000003 } }, "plans": { "starter": { "label": "Starter Plan", "entitlements": { "seats": { "limit": { "credit": "seat", "value": 3 } }, "ai-chat": { "limit": { "credit": "token", "value": 1000, "mode": "soft", "resets": true, "reset_inc": "1hr" } } } } } }`; ## Run it export const example = { title: 'quickstart', temp: true, showPolicy: false, policyFormat: 'json', policy: policyJson, stof: ` // policy: { ... } #[main] fn main() { // A customer object owns state information: meter values, overrides, plan reference, etc. self.policy.create_customer('acme_corp', 'starter'); assert(self.policy.allow('acme_corp', 'seats', 2)); pln('2 seats — allowed'); assert(self.policy.allow('acme_corp', 'ai-chat', 10000)); pln('10,000 tokens — allowed, charged for 9k in overage'); if (self.policy.allow('acme_corp', 'seats', 2)) { throw('unexpectedly allowed'); } else { pln('blocked, over the limit'); } } `, typescript: ` const policy = await Limitr.new(\`"policy": { ... }\`, 'json'); // For Limitr Cloud: await Limitr.cloud({ token: apiKey }) instead. Nothing below changes. // A customer object owns state information: meter values, overrides, plan reference, etc. await policy.createCustomer('acme_corp', 'starter'); if (await policy.allow('acme_corp', 'seats', 2)) { console.log('2 seats — allowed'); } if (await policy.allow('acme_corp', 'chat_tokens', 10000)) { console.log('10,000 tokens — allowed, charged for 9k in overage'); } if (await policy.allow('acme_corp', 'seats', 2)) { throw Error('unexpectedly allowed'); } else { console.log('blocked, over the limit'); } `, }; ## What just happened We created one customer, `acme_corp`, on the `starter` plan — enforcing 3 seats max, 1000 included tokens per hour, and $0.00004 per token for everything over. The code is written such that we could change, for example, the `ai-chat` `soft` limit to `hard` for some customers/plans and `allow` for 10k would have returned `false`. That change would be a config only change, and your code would remain the way it currently is. This is a small example, but the same pattern holds for much more involved use cases. :::tip Integrating Limitr into your code, in the vast majority of use-cases, involves just three things: customer ID (user/agent state object), entitlement name, and usage quantity. Plans are groups of entitlements. An entitlement can map to a feature, a meter for monetization, a monthly or daily limit, etc. and can always be stacked to work together. One common pattern for example: a hard limit on self-counted tokens to gate AI usage before vendor calls, then soft limit entitlements (value of 0, so everything is overage) for accurately billing AI input & output tokens based on the real counts that the vendor responds with. An entitlement without a limit is a boolean flag — quantity can be ommitted in that case. ::: --- ## Welcome Welcome to the Limitr docs. This is where you'll find everything needed to integrate Limitr — the policy spec, SDK reference, and materials for both the open-source engine and Limitr Cloud. If you already know what you're doing, jump straight into the **[Quickstart](./quickstart)** or **[Policy Reference](./reference/policy)**. Otherwise, two minutes here will save you some backtracking later. ## Scope of these docs :::info These docs cover the **open-source enforcement engine** & **technical integration** — the embedded runtime itself, available on [GitHub](https://github.com/dev-formata-io/limitr). If you're looking for the **Limitr Cloud** web application user guides — the managed dashboard, policy editor, and analytics — that's under [**Guides**](/guides/mapping), not here. Integration is the same either way, and you're in the right place for it. The SDK calls you write — `allow`, `check`, `createCustomer`, all of it — don't change based on which one you're using. The only difference is *where the policy is defined and where stored customer state lives*: self-hosted, that's a file and storage you own; on Cloud, it's fully managed for you. In code, the entire difference comes down to one line — `Limitr.new(...)` for self-hosted, `Limitr.cloud(...)` for Cloud. Everything after that call is identical. ::: ## Live code examples Our docs include live code examples — not a screenshot, not a pseudocode sketch. The actual open-source Limitr engine, running live in your browser as WebAssembly, operating just how it will inside your own applications and services. Limitr engine uses our open-source project, [Stof](https://stof.dev), to make this happen. Here's an example with just Stof to see it run, and what the text format looks like: export const example = { title: 'hello.stof', temp: true, showPolicy: false, stof: ` welcome: { message: 'Hello from Stof' hello: ()=>{ pln(self.message + ' — running live, right in your browser.'); } } #[main] fn example() { self.welcome.hello(); let sum = 0; for (const i in 5) sum += i; pln(\`0 + 1 + 2 + 3 + 4 = \${sum}\`); } `, }; :::tip[Do I need to know Stof to use Limitr?] No. Stof is what actually runs, however, **you do not need to learn Stof to use Limitr**. For policy documents, JSON, YAML, TOML, or Stof work just fine with `Limitr.new(...)`, and the SDKs eliminate the need to use or write Stof directly. It is always available, however, for additional custom tooling, embedded & overridden business logic, and Limitr power-users. Our SDKs are a thin wrapper around the Limitr Stof WASM module, shared between them. The behaviors of one SDK are identical to any other, because its literally the same runtime. ::: ## Where the engine comes from — CJ, Co-Founder & CEO Before Limitr, we were building an AI product that was usage-heavy, and monetizing it became a real limiting factor in our ability to become profitable and ship features at the pace our market demanded. Nothing on the market gave us the fine-grained visibility and control that AI & modern usage-based pricing actually needs, and every pricing change came with a growing product & eng overhead. The vision I had was a config document to define packaging, prices, credits, token limits, overhead costs, and everything that's needed for reasonable & dynamic decisions about what every user and agent can do, how much usage they get at every moment, and what it should cost them (and us). Easy to reason about, and quick to change. I'd already built Stof (the runtime under Limitr) for a different reason — a smart AI context layer, out of a background in graphics, parametric file formats, and language runtimes. It enables us to put usage control logic directly into each config, and sandbox it to the local config/context that contains it. Customer state and all of the rules that dictate it are isolated, local, extensible, and yours, forever. Many iterations and projects later, that pricing-as-config is what became Limitr. And it's the single config that enables us and all of our users to monetize, control, and analyze usage across every product and customer (in all shapes and forms, internal & external). The vision has expanded, but our mission remains the same: to provide flexible, reliable, and secure usage infrastructure for profitable software products, the teams behind those products, and the users of those products. If this resonates, feel free to book a call — I'd love to hear how it's going for you. ## What's in these docs - [**Quickstart**](./quickstart) — install the SDK, write a policy, make your first `allow()` call. Start here if you're integrating today. - [**Policy Spec**](./reference/policy) — the full policy language and schema reference. What you'll come back to once you're past the basics. - [**Guides**](/guides/mapping) — user guides, cloud docs, monetization strategy, and all things not integration-related. --- ## Persisting and Restoring Customer State Customer state management. The Quick Start creates customers and runs enforcement in a single process. In production, your process restarts — deployments, crashes, scaling events. When it does, all in-memory customer state is gone: meters, grants, overrides. This guide covers how to persist that state and restore it correctly, and how the workflow differs between the local engine and Cloud. --- ## Cloud vs. local: understand the difference first \{#cloud-vs-local} **If you're using Limitr Cloud, customer state is stored and managed by Cloud.** You don't snapshot to a database, you don't restore on startup, and you don't worry about state loss on process restart. Cloud is the source of truth and is turn-key. What you still need in Cloud mode is to load the customer into the local policy before the first enforcement call. `ensureCustomer()` handles this — it checks whether the customer is loaded locally, fetches them from Cloud if not, and creates them if they don't exist anywhere yet. **If you're using the local engine, customer state lives entirely in-process.** `policy.customers()` is your snapshot mechanism. You're responsible for persisting it and loading it on startup. --- ## `ensureCustomer()` is the right call in both modes \{#ensurecustomer} Before any enforcement call for a customer, call `ensureCustomer()`. This is the preferred way to work with customers in Limitr — not `createCustomer()`. Here's what `ensureCustomer()` does, in order: 1. Checks whether the customer is already loaded into the local policy. If yes — **no-op, returns `false`**. 2. If a Cloud WebSocket is open, tries to fetch the customer from Cloud. If found — loads them locally, **returns `false`**. 3. If not found in Cloud (or no Cloud connection), creates the customer locally and registers them with Cloud if connected. **Returns `true`**. ```typescript // Safe to call on every request — only does work when necessary await policy.ensureCustomer(userId, 'starter'); ``` `createCustomer()` has none of these guards. It will attempt to create the customer regardless of whether they already exist, and in Cloud mode it will not fetch an existing Cloud customer. Use `ensureCustomer()` everywhere except in explicit one-time provisioning flows where you know the customer is new. The return value tells you whether a customer was just created — useful for running first-time setup only for new customers: ```typescript const isNew = await policy.ensureCustomer(userId, 'starter'); if (isNew) { await policy.ensureCustomerIncludedTopups(userId); await policy.ensureCustomerPlanQuantity(userId); // add org seat, send welcome email, etc. } ``` --- ## Local engine: persisting state \{#local-persist} The local engine stores all customer state in-process. `policy.customers()` returns a complete snapshot — every customer's plan, meters, grants, overrides, and metadata. ```typescript const snapshot = await policy.customers(); // { // 'user_abc': { id: 'user_abc', plan: 'growth', meters: {...}, grants: {...}, ... }, // 'user_xyz': { id: 'user_xyz', plan: 'starter', meters: {...}, ... }, // } ``` ### When to snapshot **On every enforcement event** — the safest approach. Never lose more than one request worth of state. Use `addHandler()` to snapshot on `meter-changed`: ```typescript policy.addHandler('persist', async (key: string) => { if (key === 'meter-changed') { const snapshot = await policy.customers(); await db.set('limitr:customers', JSON.stringify(snapshot)); } }); ``` **On a periodic schedule** — acceptable for lower-traffic applications where losing a few seconds of meter state is tolerable. Simpler to reason about, cheaper to run: ```typescript setInterval(async () => { const snapshot = await policy.customers(); await db.set('limitr:customers', JSON.stringify(snapshot)); }, 10_000); // every 10 seconds ``` **On graceful shutdown** — always snapshot on `SIGTERM` regardless of which strategy you use: ```typescript process.on('SIGTERM', async () => { const snapshot = await policy.customers(); await db.set('limitr:customers', JSON.stringify(snapshot)); process.exit(0); }); ``` ### Where to store the snapshot The snapshot is a plain JSON object. Any key-value store works. **Redis** — The natural fit for most production stacks. Fast reads on startup, atomic writes, optional TTL for inactive customer cleanup. ```typescript await redis.set('limitr:customers', JSON.stringify(snapshot)); ``` **Postgres / your existing database** — Fine if Redis isn't in your stack. A single `jsonb` column on a `limitr_state` table is sufficient. ```typescript await db.query( 'INSERT INTO limitr_state (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2', ['customers', JSON.stringify(snapshot)] ); ``` **Filesystem** — Reasonable for single-instance deployments. Not suitable for horizontally scaled services where multiple instances share state. --- ## Local engine: restoring state on startup \{#local-restore} Load the snapshot into the policy before you start handling requests. `loadCustomers()` accepts the record returned by `policy.customers()` or an array of customer objects. ```typescript const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); // Restore persisted state before accepting traffic const stored = await db.get('limitr:customers'); if (stored) { await policy.loadCustomers(JSON.parse(stored)); console.log('Customer state restored'); } // Now safe to handle requests app.listen(3000); ``` `loadCustomers()` calls `setCustomer()` for each customer in parallel. It does not fire `customer-set` events — state restoration is not an event source. ### What gets restored Everything in the customer record: plan assignment, meter values and reset timestamps, grant balances and expiry, overrides, alt IDs, refs, and metadata. After `loadCustomers()`, the policy behaves as if the process never restarted. ### What doesn't get restored If the policy document changed between restarts — new plan, different limit values, renamed credits — and you load old customer state, the meters are still valid. Limitr resolves meters against the current policy at enforcement time. A customer with 400,000 tokens metered against a plan that now limits at 300,000 will be immediately at their limit, which is correct. The case to watch: if you rename an entitlement (e.g. `ai_tokens` → `ai_input`), meters saved under the old name won't map to the new entitlement. This is another reason to choose stable entitlement names from the start — see [Mapping Your Pricing Model to a Policy](./mapping#hard-to-change). --- ## Cloud mode: startup workflow \{#cloud-startup} In Cloud mode you don't restore from a snapshot — Cloud already has the state. Your startup sequence is simpler: ```typescript const policy = await Limitr.cloud({ token: process.env.LIMITR_TOKEN }); if (!policy) throw new Error('Cloud connection failed'); // That's it. Start handling requests. // ensureCustomer() will load each customer on first access. app.listen(3000); ``` The first enforcement call for any customer triggers a Cloud fetch if the customer isn't locally loaded. The customer is fetched, loaded locally, and all subsequent calls for that customer are in-process. ### Warming the cache If you have a small, known set of high-traffic customers, you can pre-load them at startup rather than waiting for the first request: ```typescript const policy = await Limitr.cloud({ token: process.env.LIMITR_TOKEN }); const activeCustomers = await db.query( 'SELECT id, plan FROM customers WHERE active = true LIMIT 1000' ); await Promise.all( activeCustomers.rows.map(row => policy.ensureCustomer(row.id, row.plan)) ); app.listen(3000); ``` This is optional — `ensureCustomer()` handles on-demand loading correctly. It's a latency optimization for the first request per customer, not a correctness requirement. ### Disconnection behavior If the Cloud WebSocket drops mid-traffic, `ensureCustomer()` for customers not yet loaded locally behaves according to `denyUnconnected`: | Setting | Behavior | |---|---| | `denyUnconnected: true` (default) | `ensureCustomer()` returns `false` without creating or fetching. Subsequent `allow()` calls deny for customers not locally loaded. | | `denyUnconnected: false` | `ensureCustomer()` creates the customer locally with the provided plan. When the connection restores, Cloud state re-syncs. | For most products, the default is correct. A customer who can't be verified against Cloud shouldn't be served as if they have a fresh plan with no consumption history. --- ## Horizontally scaled deployments \{#horizontal-scale} In a multi-instance deployment, each process has its own in-memory policy state. With the local engine, each instance snapshots and restores independently — state is not shared between instances. The simplest correct approach for horizontally scaled local deployments is **sticky sessions by customer ID** — route each customer to the same instance on every request. This keeps the meter state consistent without cross-instance coordination. If sticky sessions aren't an option, you need a shared state strategy: snapshot to Redis on every meter change, and read from Redis as the source of truth for current meter values. This adds latency on every read. :::tip[At this point, use Cloud] If you're dealing with horizontal scale and can't use sticky sessions, Limitr Cloud is almost certainly the better answer. Cloud was designed for this case — the WebSocket syncs customer state across all connected instances automatically, with no Redis coordination layer required. ::: --- ## Mapping Your Pricing Model to a Policy The first step. The first real work in a Limitr integration isn't writing code — it's thinking. Before you call `allow()` or write a YAML file, you need to translate your existing pricing model into policy concepts. This guide walks through that translation process. By the end you'll have a clear picture of what your credits, entitlements, and plans should look like, and why. :::tip[Limitr Cloud] Limitr Cloud exposes an easier workflow for creating and maintaining credits and plans. That said, it's still important to map your pricing model with intention from the start — some things are easier to change than others once customers are running against a policy. ::: --- ## Start with what you're selling, not how you're enforcing it \{#start-here} The most common mistake is starting with enforcement code and working backward to the policy. That produces a policy that mirrors your current implementation — which means it inherits all the same problems: limits in the wrong place, credits that conflate different resources, plans that are hard to extend. Start instead with two questions: **What does your product deliver?** Not features, not plans — the actual unit of value customers consume. AI output, API calls, storage bytes, compute seconds, seats, reports. This maps to credits. **What are customers allowed to consume?** What does each plan include? What happens at the boundary — do they get blocked or do they get billed? What resets, and when? This maps to entitlements and limits. Answer both questions before you open the policy editor. --- ## Identify your credits \{#credits} Credits are the units your pricing is built on. Every limit is denominated in a credit. Every meter tracks consumption in a credit. For each thing your product delivers, ask: ### Discrete or abstract? A **discrete credit** maps to something real with a known cost: an input token, a GPU second, a megabyte stored. It should have `overhead_cost` (what you pay the provider per unit) and `price` (what you charge per unit in overage). This is how Limitr tracks margin per customer. An **abstract credit** is a conceptual unit your business or customers understand: "AI Credits", "messages", "compute units", "successful runs". It maps to discrete credits through the exchange table, and may or may not have a price. Most products need both — one or more discrete credits per resource type, and optionally one abstract credit per customer-facing unit. ### Should you split it? The default instinct is to combine — "tokens are tokens", "API calls are API calls." This is almost always wrong for anything with meaningful cost variation. Policies support hundreds of credits. It's easier to manage a policy with flexibility built in, which may mean a different discrete credit type per endpoint, per outcome, or per feature. **Split credits when:** - The cost profile differs (input tokens are 4x cheaper than output tokens for most models) - The pricing model differs (tiered output, flat input) - You want independent limits (cap Sonnet separately from Haiku) - You need per-resource visibility in margin tracking **Don't split** when the resource is genuinely uniform and you have no reason to treat its components differently. **Common splits that pay off:** | Split | Why | |---|---| | AI input / AI output tokens | Different cost, different price, often different limit logic | | Storage / bandwidth | Storage accumulates; bandwidth resets monthly | | Standard API / expensive API calls | Different cost profile, different plan access | | Per-model credits | Different price points across models | | Premium AI tools / standard workflows | Different context per workflow | --- ## Map your plans \{#plans} For each plan, work through every entitlement: **What does this plan include?** The baseline allocation — 500,000 tokens/day, 5 seats, 1 GiB storage. This becomes `limit.value`. **What happens at the boundary?** | Mode | When to use it | |---|---| | `hard` | Block immediately. No overage. Use for free tiers, trial limits, hard caps. | | `soft` | Allow overage and bill for it. Use for Growth and Enterprise plans where customers pay for what they use. A soft limit of `0` generates overage immediately. | | `observe` | Meter with no enforcement. Use for enterprise "unlimited" tiers, usage reporting, or to gain visibility before deciding to enforce. | **What resets, and when?** - Consumption metrics (tokens, API calls, compute time) reset. Set `resets: true` and `reset_inc` to match your billing window. - State metrics (storage, seats) don't reset. Set `resets: false` — the meter reflects what's currently in use. **Feature flag or metered resource?** An entitlement with no `limit` is a boolean gate — the customer either has it or doesn't. Use this for feature access (`pdf_export`, `sso`, `advanced_analytics`) where the question is plan eligibility, not consumption. --- ## A working example \{#example} Suppose your current pricing looks like this: > **Starter — $49/mo:** 500K AI tokens/day, API access, basic integrations > **Growth — $149/mo:** 2M AI tokens/day, overage at $0.004/1K tokens, all integrations, priority support > **Enterprise — Custom:** Unlimited tokens, custom limits, SSO, audit log, dedicated support ### Credits You're selling "AI tokens" — but tokens aren't uniform. Input and output have different costs. Split them: ```yaml credits: ai_input: overhead_cost: 0.000003 # $3/1M tokens (your cost) pricing_model: flat price: { amount: 0.000004 } stof_units: int resets: true ai_output: overhead_cost: 0.000015 # $15/1M tokens (your cost) pricing_model: flat price: { amount: 0.00002 } stof_units: int resets: true ``` Your pricing page says "$0.004/1K tokens" in overage — that's an average across input and output. You can honor that average by setting both credit prices to `0.000004`, or split the price accurately. Accurate is better: it protects your margin as output ratios shift. ### Plans ```yaml plans: starter: entitlements: api_access: description: API access # boolean flag — no limit basic_integrations: description: Basic integrations # boolean flag ai_input: limit: credit: ai_input mode: hard # Starter hits a wall — no overage value: 500000 resets: true reset_inc: 1day ai_output: limit: credit: ai_output mode: hard value: 200000 resets: true reset_inc: 1day growth: entitlements: api_access: description: API access advanced_integrations: description: Advanced integrations # Growth-only feature flag ai_input: limit: credit: ai_input mode: soft # Growth overages and gets billed value: 2000000 resets: true reset_inc: 1day ai_output: limit: credit: ai_output mode: soft value: 800000 resets: true reset_inc: 1day enterprise: entitlements: api_access: description: API access advanced_integrations: description: Advanced integrations sso: description: SAML/SSO # Enterprise-only flag audit_log: description: Audit log access # Enterprise-only flag ai_input: limit: credit: ai_input mode: observe # Meter everything, enforce nothing resets: true reset_inc: 1day ai_output: limit: credit: ai_output mode: observe resets: true reset_inc: 1day ``` Notice what happened: "Unlimited tokens" on Enterprise becomes `observe` mode — not a missing entitlement or an arbitrarily high limit. You still want the meter for margin tracking and Cloud dashboards. You just don't want enforcement. :::tip[Cloud pricing rules] In Limitr Cloud, you can create a pricing rule that overrides the cost of any credit for dynamic groups of customers — for example, any customer with `metadata.account_type == "enterprise"` gets 100% off AI input tokens for the next 42 days — without changing the plan or writing code. ::: --- ## The decisions that are hard to change later \{#hard-to-change} Some policy decisions are easy to evolve — you can change a limit value or add a plan without touching your application code, especially with Cloud. Others require more upfront thought because changing them later affects customer state. **Credit granularity is hard to change.** If you start with a single `ai_token` credit and later want to split input and output, every customer's meter is in the wrong unit. Design credits at the granularity you'll want for reporting and margin tracking, even if your current pricing page doesn't expose that detail. **Entitlement names are referenced in code.** `policy.allow(userId, 'ai_tokens')` is in your request handlers. If you rename `ai_tokens` to `ai_input` later, that's a code change and a deploy. Choose names that are stable and specific enough to survive plan evolution. Credits evolve independently — you can always change the credit (and therefore the price) for an entitlement without touching code. **Hard limits on Growth are a trap.** If your Growth plan has hard limits today but you want to introduce overage billing later, you'll need to change the limit mode. That's a policy change — easy with Cloud, but it affects all Growth customers immediately. Think about where you want the enforcement boundary before you ship. **The exchange table is additive.** You can always add new credits and exchange paths. However, changing the value of an existing exchange pair affects how existing grants are drawn. Set customer-facing credit exchanges up correctly from the start. --- ## Checklist \{#checklist} Run through this before you write the policy file: - Every resource your product delivers is represented as a credit - Resources with different cost profiles are separate credits - Resources with different reset behavior are separate credits - Every plan's limits are expressed as `hard`, `soft`, or `observe` — not just a value - Feature access is modeled as boolean entitlements (no limit), not high-value hard limits - Enterprise "unlimited" is `observe` (or a Cloud pricing rule), not a missing entitlement - Entitlement names are stable and descriptive enough to live in your codebase long-term - `overhead_cost` values reflect real provider costs, not approximations Once this list is clear, the policy file writes itself. --- ## Handling Plan Changes Upgrades, downgrades, and mid-period plan switches. Upgrades, downgrades, and mid-period plan switches are where pricing complexity tends to surface. Limitr gives you the right primitives — the decisions are about when to reset meters, what to do with active grants, and how to sequence the calls correctly so your billing system stays in sync. --- ## The core operation \{#core} `setCustomerPlan(id, planId, overwrite_meters?)` is the single call that changes a customer's plan. It returns `true` if the plan actually changed, `false` if the customer was already on that plan. On a successful change it fires two events: `customer-set` and `customer-plan-changed`. ```typescript const changed = await policy.setCustomerPlan('user_abc', 'growth'); // true if changed, false if already on growth ``` :::note[Limitr Cloud] Cloud depends on the `customer-set` event only, so `setCustomer(customer)` — which sets the entire customer object at once — also works fine for plan changes in Cloud mode. ::: --- ## `overwrite_meters`: the most important decision \{#overwrite-meters} The third argument to `setCustomerPlan()` defaults to `true`. This is the most consequential decision in a plan change. | Value | Behavior | |---|---| | `true` (default) | Resets all meter values to zero. Customer starts fresh on the new plan. | | `false` | Preserves current meter values. Consumption carries forward onto the new plan's entitlements. | Neither is universally correct. The right choice depends on what the plan change means in your product. ### When to reset meters (`true`) **Upgrades where the new plan is a clean break.** A customer on Starter (500K tokens/day hard limit) upgrades to Growth (2M tokens/day soft limit). They've used 450K tokens today. Resetting gives them the full 2M allocation immediately — the right experience for an upgrade. They paid for more; they should get more now. **Downgrades at period end.** If you're processing a scheduled downgrade at billing period renewal, resetting meters is correct — the new period starts fresh. **Trial-to-paid conversions.** A customer converting from trial to paid should start with clean meters on their first paid period. ### When to preserve meters (`false`) **Mid-period downgrades where consumption should count.** A customer downgrades from Growth to Starter mid-month. They've used 300K tokens today against Growth's 2M limit. If you reset, they'd get a fresh 500K Starter allocation immediately — effectively a free day of extra capacity as a reward for downgrading. Preserving the meter means their 300K counts against the Starter limit of 500K, leaving them 200K for the rest of the day. **Lateral plan switches.** Moving a customer between plans at the same price point where you want consumption to be continuous. **When you're handling proration manually.** If your billing provider is computing a proration credit, preserving meters keeps usage data coherent with what was billed. --- ## The full upgrade sequence \{#upgrade} ```typescript async function upgradeCustomer(customerId: string, newPlan: string) { // 1. Change the plan — reset meters on upgrade const changed = await policy.setCustomerPlan(customerId, newPlan, true); if (!changed) return { success: false, reason: 'Already on this plan' }; // 2. Sync included topups for the new plan // Adds grants for topups included on the new plan. // Removes grants from topups that were on the old plan but aren't on the new one. await policy.ensureCustomerIncludedTopups(customerId); // 3. Trigger the subscription charge for the new plan // Only if your plans have a subscription entitlement set up. // No-op if the meter is already >= 1. await policy.ensureCustomerPlanQuantity(customerId); // 4. Update your billing provider // Do this after Limitr state is set — Limitr is the source of truth // for what plan the customer is on. Don't let a billing failure leave // Limitr and your billing provider out of sync. await billing.updateSubscription(customerId, newPlan); return { success: true }; } ``` ### Why this sequence **Plan first, topups second.** `ensureCustomerIncludedTopups()` reads the customer's current plan to determine which topups are applicable. Call it after `setCustomerPlan()`, not before. **Subscription charge after plan change, before billing provider.** `ensureCustomerPlanQuantity()` fires the `meter-overage` event your billing handler uses to queue the charge. If this fires before `setCustomerPlan()`, the event payload references the old plan. **Handle the `false` return.** If the customer is already on the target plan, don't re-run the topup and subscription logic — it's a no-op at best, a double-charge at worst. --- ## The full downgrade sequence \{#downgrade} ```typescript async function downgradeCustomer(customerId: string, newPlan: string) { // Preserve meters on downgrade — consumption should count against the new plan const changed = await policy.setCustomerPlan(customerId, newPlan, false); if (!changed) return { success: false, reason: 'Already on this plan' }; // Sync topups — removes included grants no longer applicable on the new plan. // Purchased grants (not tied to an included topup) are not removed. await policy.ensureCustomerIncludedTopups(customerId); // No subscription charge here — downgrades typically don't trigger a new charge. // Handle any proration credit in your billing provider separately. await billing.updateSubscription(customerId, newPlan); return { success: true }; } ``` ### Grant behavior on downgrade `ensureCustomerIncludedTopups()` removes grants for topups no longer on the customer's plan — but **only for `included` topups**. Grants created from purchased topups (where the customer paid explicitly) are not removed. They remain on the customer and continue to be drawn against applicable overages on the new plan if the exchange table allows it. To remove all grants on downgrade regardless of origin, do so explicitly before calling `ensureCustomerIncludedTopups()`. --- ## Mid-period plan changes and meters \{#mid-period} When a customer changes plans mid-period and you preserve meters, their current consumption may already exceed the new plan's limits. This is handled correctly: - If the new plan's limit is **`hard`** and the meter already exceeds it, the next `allow()` call returns `false` immediately. - If the new plan's limit is **`soft`** and the meter exceeds it, the customer is already in overage. The next `allow()` fires `meter-overage`. - If the new plan's limit is **`observe`**, nothing changes — the meter continues accumulating. In all cases the meter state is coherent. Whether this is the right behavior for your product is a business decision, not a technical one. --- ## Scheduled plan changes \{#scheduled} Some products defer plan changes to the end of the billing period. Limitr doesn't have a built-in scheduler — schedule the change in your own infrastructure and call `setCustomerPlan()` when the period ends: ```typescript // In your billing renewal worker async function processBillingRenewal(customerId: string) { const account = await db.getAccount(customerId); // Apply any pending plan change at period start if (account.pendingPlan && account.pendingPlan !== account.currentPlan) { await policy.setCustomerPlan(customerId, account.pendingPlan, true); await policy.ensureCustomerIncludedTopups(customerId); await db.clearPendingPlan(customerId); } // Trigger subscription charge for the new period await policy.ensureCustomerPlanQuantity(customerId); } ``` --- ## Per-customer overrides as an alternative \{#overrides} Sometimes you don't need to change the plan — you need to adjust a single limit for a specific customer. Enterprise deals, pilot customers, support resolutions. `createCustomerOverride()` changes one entitlement's limit without touching the plan or the meters: ```typescript // Give this customer 2x their plan's token limit, expiring in 30 days await policy.createCustomerOverride( 'user_abc', 'chat_input', 4000000, Date.now() + 30 * 24 * 60 * 60 * 1000 // expires_on ); // Revert to plan default await policy.removeCustomerOverride('user_abc', 'chat_input'); ``` Overrides are the right tool when the change is customer-specific and temporary. Plan changes are the right tool when the change reflects a different product tier. :::tip[Watch for override sprawl] If you find yourself managing overrides at scale — giving everyone on a "growth-plus" arrangement an override rather than a proper plan — that's a signal you need a new plan, not more overrides. ::: --- ## Testing Your Policy Self-hosted testing. :::tip[Using Limitr Cloud?] Policy validation is built into the Cloud publishing workflow. Cloud maintains separate test and live environments — make changes in test, verify behavior in your development environment connected to the test policy, publish to live when it's right. You don't need a separate test suite for policy correctness in Cloud. The rest of this guide is for developers using the **local open source engine**. ::: --- ## What the built-in validator catches \{#validator} `Limitr.new()` validates your policy by default before returning. If the policy is invalid, it throws with the validation error. ```typescript try { const policy = await Limitr.new(policyYaml, 'yaml'); } catch (err) { console.error('Invalid policy:', err.message); // e.g. 'A credit named "sonnet_inpput" does not exist in this policy' } ``` You can also call `valid()` explicitly on an already-loaded policy: ```typescript const [valid, error] = await policy.valid(); if (!valid) console.error(error); ``` The validator checks credit references, unit strings, limit and topup values, tier structure, entitlement and topup schema conformance, and trial period configuration. What the validator does **not** check: - Whether your credit prices and costs make economic sense - Whether your limits are appropriate for your product - Whether your exchange table chains resolve correctly for your use case - Whether enforcement behaves the way you expect at runtime Those require behavioral tests. --- ## Validation as a CI gate \{#ci} The simplest thing you can do: load your policy file in CI and let the built-in validator fail the build on an invalid policy. No test framework required. ```typescript title="scripts/validate-policy.ts" const policyPath = process.argv[2] ?? './policy.yaml'; try { const policy = await Limitr.new(readFileSync(policyPath, 'utf-8'), 'yaml'); const version = await policy.version(); console.log(`✓ Policy valid (Limitr ${version})`); process.exit(0); } catch (err) { console.error(`✗ Invalid policy: ${(err as Error).message}`); process.exit(1); } ``` ```json title="package.json" "scripts": { "validate": "bun run scripts/validate-policy.ts policy.yaml" } ``` ```yaml title=".github/workflows/ci.yml" - name: Validate Limitr policy run: bun run validate ``` --- ## Writing enforcement tests \{#tests} Enforcement tests verify that `allow()`, `increment()`, and `check()` behave correctly for the scenarios your product depends on. Use your existing test framework — Limitr is just TypeScript. ### Setup pattern ```typescript async function loadPolicy() { return Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); } ``` Create a fresh policy instance per test or test file. Limitr is in-process — there's no server to reset and no shared state. Each `Limitr.new()` call is a clean slate. ### Testing hard limits ```typescript test('starter hard limit blocks at 500K input tokens', async () => { const policy = await loadPolicy(); await policy.createCustomer('test_user', 'starter'); const allowed = await policy.allow('test_user', 'chat_input', 500_000); expect(allowed).toBe(true); const blocked = await policy.allow('test_user', 'chat_input', 1); expect(blocked).toBe(false); const remaining = await policy.remaining('test_user', 'chat_input'); expect(remaining).toBe(0); }); ``` ### Testing soft limits and events ```typescript test('growth soft limit fires meter-overage event', async () => { const policy = await loadPolicy(); await policy.createCustomer('test_user', 'growth'); const events: string[] = []; policy.addHandler('test', (key) => events.push(key)); await policy.allow('test_user', 'chat_input', 2_000_000); // at limit await policy.allow('test_user', 'chat_input', 1); // over limit expect(events).toContain('meter-overage'); expect(events).not.toContain('meter-limit'); }); ``` ### Testing grant coverage ```typescript test('grant covers overage before meter-overage fires', async () => { const policy = await loadPolicy(); await policy.createCustomer('test_user', 'growth'); await policy.applyCustomerTopup('test_user', 'monthly_credits'); const overageEvents: unknown[] = []; policy.addHandler('test', (key, value) => { if (key === 'meter-overage') overageEvents.push(JSON.parse(value as string)); }); await policy.allow('test_user', 'chat_input', 2_000_000); // at limit await policy.allow('test_user', 'chat_input', 100_000); // into overage — covered by grant expect(overageEvents).toHaveLength(0); // grant covered it await policy.allow('test_user', 'chat_input', 10_000_000); // exhaust the grant expect(overageEvents.length).toBeGreaterThan(0); }); ``` ### Testing boolean feature gates ```typescript test('starter plan does not have advanced analytics', async () => { const policy = await loadPolicy(); await policy.createCustomer('test_starter', 'starter'); await policy.createCustomer('test_growth', 'growth'); expect(await policy.check('test_starter', 'advanced_analytics')).toBe(false); expect(await policy.check('test_growth', 'advanced_analytics')).toBe(true); }); ``` ### Testing customer overrides ```typescript test('customer override replaces plan limit', async () => { const policy = await loadPolicy(); await policy.createCustomer('test_user', 'starter'); expect(await policy.limit('test_user', 'chat_input', false)).toBe(500_000); await policy.createCustomerOverride('test_user', 'chat_input', 1_000_000); expect(await policy.limit('test_user', 'chat_input', false)).toBe(1_000_000); const allowed = await policy.allow('test_user', 'chat_input', 750_000); expect(allowed).toBe(true); await policy.removeCustomerOverride('test_user', 'chat_input'); expect(await policy.limit('test_user', 'chat_input', false)).toBe(500_000); }); ``` --- ## Testing policy changes with `difference()` \{#difference} `difference()` computes a structured diff between two policy instances — useful for verifying that a policy change affects exactly what you intended and nothing else. ```typescript test('adding enterprise plan does not change starter or growth', async () => { const before = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); const after = await Limitr.new(readFileSync('./policy.new.yaml', 'utf-8'), 'yaml'); // before is treated as the schema for the diff const diff = await before.difference(after); const changedKeys = Object.keys(diff); expect(changedKeys).toContain('enterprise'); expect(changedKeys).not.toContain('starter'); expect(changedKeys).not.toContain('growth'); }); ``` `difference(other, symmetric?)` — when `symmetric` is `false` (default), only changes relative to the calling policy's structure are returned. When `true`, changes in both directions are included. --- ## What to test before shipping a pricing change \{#shipping} When changing limits, adding a plan, or adjusting credit prices — not just authoring a policy for the first time — test the specific change: **Limit changes** — Test that the new limit blocks or allows at the right value. Test that existing customers whose meters are already near the old limit behave correctly under the new one. **New plan** — Test that all entitlements gate correctly. Test that a customer on the new plan can't access entitlements from other plans. Test upgrade and downgrade paths to and from the new plan. **Credit price changes** — These don't affect enforcement, but verify `customerMarginSnapshot()` returns the expected margin values if your billing code depends on it. **Exchange table changes** — Test that `creditExchange()` returns the expected conversion values. Test that grants are drawn correctly at the new exchange rate. **New topup** — Test that `applyCustomerTopup()` creates a grant with the right starting value. Test that the grant is drawn before overage fires. Test expiry if `expires_after` is set. --- ## Writing Custom Alert Conditions Limitr's notification and alerting system. Limitr's notification system lets you define precisely which events your application cares about and what should happen when they fire. Conditions are written in Stof — a lightweight language that runs inside the policy engine. :::tip[Limitr Cloud] Alert conditions can be defined in the dashboard and configured to alert your team in real time via Slack or email. The in-process patterns below work in Cloud mode too — Cloud-synced conditions and local `setNotifications()` conditions coexist on the same policy instance. ::: --- ## How notifications work \{#how-it-works} Every enforcement call that updates a meter fires one of three event types: `meter-changed`, `meter-limit`, or `meter-overage`. For each event, Limitr calls `matches(type, event)` on every notification defined in the policy. If `matches()` returns `true`, the notification's `fire` function is called, and your TypeScript `addHandler()` receives the result with the notification name as the `key`: ```typescript policy.addHandler('alerts', (key: string, value: unknown) => { // key is a built-in event type OR a custom notification name if (key === 'high-usage-warning') { const event = JSON.parse(value as string); slack.send(`#alerts`, `${event.customer.id} is at ${event.remaining} tokens remaining`); } }); ``` --- ## The Stof notification format \{#format} Notifications are loaded via `setNotifications()` as a Stof string. Each notification is a named field containing a `matches` function and optionally a `fire` function: ```typescript await policy.setNotifications(` : { fn matches(type: str, event: obj) -> bool { // return true to fire this notification } fn fire(name: str, event: obj) { // optional — events always sent to addHandler() regardless } } `); ``` `setNotifications()` merges into the policy's `notifications` block. Calling it multiple times with the same notification ID replaces the earlier definition. --- ## The event payload \{#payload} Both `matches()` and `fire()` receive the full event object: ``` event.type // redundant with the type param, but available event.entitlement // entitlement name: 'chat_input', 'seats', etc. event.plan // plan ID: 'starter', 'growth', etc. event.remaining // remaining balance after this operation event.customer.id // customer ID event.customer.plan // customer's current plan event.customer.type // customer type: 'user', 'org', etc. event.meter.value // new meter value (post-operation) event.meter.limit // enforced limit value event.meter.credit // credit ID event.credit.description // credit description string event.overage // meter-overage only: amount over limit after grants event.grant_value_applied // meter-overage only: how much grant covered ``` --- ## Stof syntax primer \{#syntax} `matches()` returns a boolean expression. A few things to know: **Equality and comparison:** ```stof type == 'meter-changed' event.remaining < 1000 event.meter.value >= event.meter.limit ``` **Logical operators:** ```stof type == 'meter-changed' && event.entitlement == 'chat_input' type == 'meter-limit' || type == 'meter-overage' ``` **Arithmetic:** ```stof event.remaining < (event.meter.limit / 2) // less than 50% remaining event.meter.value >= (event.meter.limit * 0.8) // 80% consumed ``` **Null safety:** Field access on a missing field returns `null` rather than throwing. `null < 1000` is `false`. **Return value:** Use explicit `return` or let the last expression be the return value: ```stof fn matches(type: str, event: obj) -> bool { type == 'meter-changed' // implicitly returned } ``` --- ## Patterns \{#patterns} ### Usage threshold alert Fire when a customer has consumed more than 80% of their allocation: ```typescript await policy.setNotifications(` approaching-limit: { fn matches(type: str, event: obj) -> bool { if (type != 'meter-changed') return false; if (event.entitlement != 'chat_input') return false; event.meter.value >= (event.meter.limit * 0.8) } } `); policy.addHandler('alerts', (key, value) => { if (key === 'approaching-limit') { const event = JSON.parse(value as string); const pct = Math.round((event.meter.value / event.meter.limit) * 100); notify(`${event.customer.id} has used ${pct}% of their daily token budget`); } }); ``` ### Hard limit hit on a specific plan Fire only when a Growth customer hits a hard limit — useful for distinguishing from expected Starter blocks: ```typescript await policy.setNotifications(` growth-hard-limit: { fn matches(type: str, event: obj) -> bool { type == 'meter-limit' && event.customer.plan == 'growth' } } `); ``` ### Any limit event on a specific entitlement Catch both hard blocks and soft overages on seats — useful for org seat management: ```typescript await policy.setNotifications(` seat-pressure: { fn matches(type: str, event: obj) -> bool { event.entitlement == 'seats' && (type == 'meter-limit' || type == 'meter-overage') } } `); policy.addHandler('seats', (key, value) => { if (key === 'seat-pressure') { const event = JSON.parse(value as string); crm.flag(event.customer.id, 'seat-pressure', { type: event.type, used: event.meter.value, limit: event.meter.limit, }); } }); ``` ### First overage event only Fire on the first overage per customer per entitlement — useful for sending a single "you've exceeded your limit" notification rather than one per request: ```typescript await policy.setNotifications(` first-overage: { fn matches(type: str, event: obj) -> bool { if (type != 'meter-overage') return false; // remaining is negative when over limit — fire only when meter first crosses event.remaining >= -1 && event.remaining < 0 } } `); ``` ### Multiple entitlements, same condition ```typescript await policy.setNotifications(` token-budget-warning: { fn matches(type: str, event: obj) -> bool { if (type != 'meter-changed') return false; const watched = event.entitlement == 'chat_input' || event.entitlement == 'chat_output'; if (!watched) return false; event.remaining < (event.meter.limit * 0.1) } } `); ``` ### Async `fire` function with a registered library When you need `fire` to call a TypeScript function registered from your application: ```typescript // Register a library function from TypeScript policy.doc.lib('Alerts', 'high_usage', (customerId: string, remaining: number) => { pagerduty.trigger(customerId, `${remaining} tokens remaining`); }); await policy.setNotifications(` high-usage: { fn matches(type: str, event: obj) -> bool { type == 'meter-changed' && event.entitlement == 'chat_input' && event.remaining < 5000 } async fn fire(name: str, event: obj) { // The ? prefix is Stof's optional-call operator — no-op if not registered ?Alerts.high_usage(event.customer.id, event.remaining); } } `); ``` --- ## `fire` function signatures \{#fire-signatures} `fire` can take 2 params, 1 param, or no params — Limitr calls whichever signature is defined: ```stof fn fire(name: str, event: obj) { ... } // name = notification ID fn fire(event: obj) { ... } // event only fn fire() { ... } // no args ``` When no `fire` function is defined, Limitr routes the event directly to your `addHandler()` with the notification ID as the key — which is the most common pattern: ```typescript // No fire function — event routes directly to addHandler() await policy.setNotifications(` approaching-limit: { fn matches(type: str, event: obj) -> bool { type == 'meter-changed' && event.remaining < (event.meter.limit * 0.2) } } `); policy.addHandler('alerts', (key, value) => { if (key === 'approaching-limit') { /* ... */ } }); ``` --- ## `setNotifications` vs `addHandler` directly \{#choosing} | | `setNotifications` | `addHandler` directly | |---|---|---| | Condition language | Stof | TypeScript | | Part of versioned policy | Yes | No | | Supports Cloud routing | Yes | No | | Dynamic at runtime | Yes | Yes | | Best for | Threshold conditions, Cloud alerts | Simple event filtering, billing handlers | A common production setup: `setNotifications` for threshold-based conditions, `addHandler` for billing and operational event handling that runs regardless. --- ## Loading notifications at runtime \{#runtime} `setNotifications()` can be called at any time — before or after customers are created, before or after enforcement starts. New conditions take effect immediately on the next enforcement call. ```typescript // Load from a separate file await policy.setNotifications(readFileSync('./notifications.stof', 'utf-8')); // Or inline, updated dynamically await policy.setNotifications(` new-condition: { fn matches(type: str, event: obj) -> bool { type == 'meter-overage' && event.plan == 'enterprise' } } `); ``` --- ## Capabilities Named, callable units of policy logic. Capabilities are named, callable units of policy logic with defined input parameters and a structured output. They let you expose policy-aware functions as Claude tool definitions — Limitr generates the tool schema from the capability definition, executes the logic when Claude invokes the tool, and returns a formatted `tool_result` object ready to pass back to the API. Capabilities are also useful independently of Claude: `runCapability()` calls any capability directly from TypeScript with a plain argument map. --- ## The core concept \{#concept} A capability has: - A **name** and **description** (used in tool definitions) - **Parameters** with types and descriptions (become the tool's `input_schema`) - One or more **`#[run]`** Stof pipeline stages that execute - A **result** field name that maps to the output When `claudeToolUse()` is called with a `tool_use` block from Claude, Limitr finds the matching capability by name, injects the inputs, runs the pipeline, and returns a `tool_result` object with the serialized output. --- ## Defining a capability \{#defining} Capabilities are defined via `setCapabilities()` as a Stof string, or inline in a native Stof policy file under `capabilities:`. ### Basic capability with `#[run]` ```typescript await policy.setCapabilities(` area: { version: 0.1.0 description: 'Calculate the area of a rectangle' parameters: [ { name: 'width', description: 'Width of the rectangle', schema_type: 'number' } { name: 'height', description: 'Height of the rectangle', schema_type: 'number' } ] result: 'area' #[run] fn calculate() { self.output.area = (self.input.width as float * self.input.height as float).round(2); } } `); ``` `self.input` contains the input parameters injected by `claudeToolUse()`. `self.output` is where you write results. The `result` field names the output field that gets serialized into the `tool_result` content. ### Multi-stage pipeline with `#[run(N)]` Stages run in ascending numeric order: ```typescript await policy.setCapabilities(` area: { version: 0.1.0 description: 'Get the area of a shape from a Stof generator' parameters: [ { name: 'shape', description: 'Stof that generates a shape with an area() function' } { name: 'units', description: 'Output units for the area (e.g. "m", "ft", "in")' } ] result: 'area' #[run(0)] fn setup() { const shape = new {} on self.input; parse(self.input.shape, shape, 'stof'); self.input.shape = shape.gen(); self.input.units = self.input.units as str ?? 'float'; } #[run(1)] fn calculate() { self.output.area = self.input.shape.area().to_units(self.input.units).round(2); } } `); ``` `parse(content, target, format)` parses a string into an existing object. `to_units()` converts a value to the target unit. `round(n)` rounds to n decimal places. ### Typed capability with `#[extends]` For capabilities that share common behavior, define a base type and extend it. The `GetEndpoint` pattern builds a reusable HTTP GET capability: ```typescript // Define the base type in the document policy.doc.parse(` #[type] #[extends('Capability')] GetEndpoint: { str endpoint: ''; list query: []; #[run] fn get_request() { const query = new {}; for (const q in self.query) { const v = self.input.get(q); if (v != null) query.insert(q, v); } let endpoint = self.endpoint; if (query.len() > 0) endpoint += '?' + stringify('urlencoded', query); drop(query); const res = await Http.fetch(endpoint); self.set_result(res); } } `); // Instantiate capabilities that extend it await policy.setCapabilities(` GetEndpoint weather-forecast: { version: 0.1.0 description: 'Get a weather forecast for a location' parameters: [ { name: 'latitude', description: 'Latitude', schema_type: 'number' } { name: 'longitude', description: 'Longitude', schema_type: 'number' } ] endpoint: 'https://api.weather.example.com/forecast' query: ['latitude', 'longitude'] } GetEndpoint exchange-rate: { version: 0.1.0 description: 'Get the current exchange rate between two currencies' parameters: [ { name: 'from', description: 'Source currency code', schema_type: 'string' } { name: 'to', description: 'Target currency code', schema_type: 'string' } ] endpoint: 'https://api.fx.example.com/rate' query: ['from', 'to'] } `); ``` `GetEndpoint weather-forecast` instantiates a `GetEndpoint`-typed object named `weather-forecast`. The `#[run]` logic from `GetEndpoint` runs for both capabilities — each gets its own `endpoint` and `query` configuration. `set_result(v)` is a `Capability` method that writes to the named output field. `self.input.get(key)` reads an input parameter by name. --- ## Using capabilities with Claude \{#claude} ### Getting tool definitions `claudeTools()` returns an array of Claude-compatible tool definitions generated from all capabilities in the policy: ```typescript const client = new Anthropic(); const tools = await policy.claudeTools(); const response = await client.messages.create({ model: 'claude-opus-4-5', max_tokens: 1024, tools, messages: [{ role: 'user', content: 'What is the area of a 3ft by 4ft rectangle?' }], }); ``` To get tools for a specific customer — respecting `plans` and `customers` access filters on the capability: ```typescript const tools = await policy.claudeTools('user_abc'); ``` ### Handling tool use When Claude responds with a `tool_use` block, pass it to `claudeToolUse()`: ```typescript for (const block of response.content) { if (block.type === 'tool_use') { const toolResult = await policy.claudeToolUse(block); // { type: 'tool_result', tool_use_id: '...', content: '12' } } } ``` ### Full conversation loop ```typescript async function runWithTools(userMessage: string, customerId?: string) { const tools = await policy.claudeTools(customerId); const messages: Anthropic.MessageParam[] = [ { role: 'user', content: userMessage } ]; while (true) { const response = await client.messages.create({ model: 'claude-opus-4-5', max_tokens: 1024, tools, messages, }); const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === 'tool_use') { const result = await policy.claudeToolUse(block, customerId); if (result) toolResults.push(result as Anthropic.ToolResultBlockParam); } } if (toolResults.length === 0 || response.stop_reason === 'end_turn') { const text = response.content.find(b => b.type === 'text'); return text?.type === 'text' ? text.text : ''; } messages.push({ role: 'assistant', content: response.content }); messages.push({ role: 'user', content: toolResults }); } } const answer = await runWithTools('What is the area of a 3ft by 4ft rectangle in square meters?'); console.log(answer); ``` --- ## Calling capabilities directly \{#direct} `runCapability()` executes a capability without going through Claude — useful for testing, batch processing, or any case where you want the capability logic without an LLM in the loop: ```typescript const result = await policy.runCapability('area', { width: 3, height: 4 }); console.log(result); // { area: 12 } // With a customer ID — respects access filters const result = await policy.runCapability('weather-forecast', { latitude: 42.36, longitude: -71.06, }, 'user_abc'); ``` --- ## Capability access control \{#access-control} Capabilities can be restricted to specific plans or customer IDs using `plans` and `customers` fields: ```typescript await policy.setCapabilities(` premium-analysis: { version: 0.1.0 description: 'Advanced data analysis — Growth and Enterprise only' plans: ['growth', 'enterprise'] parameters: [ { name: 'dataset', description: 'Dataset to analyze', schema_type: 'string' } ] result: 'analysis' #[run] fn analyze() { self.output.analysis = 'Analysis result for: ' + self.input.dataset; } } `); // claudeTools() and claudeToolUse() both respect plan/customer filters // when a customerId is passed const tools = await policy.claudeTools('user_abc'); ``` --- ## Registering host functions for capabilities \{#host-functions} Capabilities run inside the Stof engine. To call TypeScript from within a `#[run]` function, register a library function with `doc.lib()`: ```typescript // Register fetch for HTTP capabilities policy.doc.lib('Http', 'fetch', async (url: string) => { const response = await fetch(url); return await response.text(); }); // Register standard output policy.doc.lib('Std', 'pln', (...args: unknown[]) => console.log(...args)); ``` These are available to all capabilities and Stof functions in the document. `Http.fetch` is what makes `GetEndpoint`-style capabilities actually execute HTTP requests — without it, `Http.fetch` is an optional call that silently no-ops. See [Embedded Event Handlers](./eventhandlers#doc-lib) for more on `doc.lib()`. --- ## Capabilities in Limitr Cloud \{#cloud} In Limitr Cloud, capabilities defined in your policy are available as part of **Limitr Network** — callable by authorized agents and other Limitr-connected services without direct SDK integration. The same capability definition works both locally (via `claudeToolUse()`) and over the network (via Cloud routing). The local SDK is the right place to develop and test capabilities. Once they're working, publishing the policy to Cloud makes them available network-wide. --- ## Embedded Event Handlers Using Stof for handlers that live within your policy document. The notification system covered in [Writing Custom Alert Conditions](./alertconditions) is built on top of a lower-level event mechanism that runs directly inside the policy document. Understanding it gives you more control over how events are routed, lets you register TypeScript functions callable from Stof, and lets you embed event logic directly into the policy without the `Notification` abstraction. --- ## Two layers of event handling \{#layers} Limitr has two layers: **Layer 1 — Attribute-decorated functions.** Functions in the policy document decorated with `#[meter-overage]`, `#[meter-limit]`, or `#[meter-changed]` fire automatically on matching events. No `setNotifications()`, no handler registration — just a function in the document with the right attribute. **Layer 2 — `App` library bridge.** When `App` is registered as a library namespace via `policy.doc.lib()`, event functions can call out to TypeScript. The built-in `AppEvents` handlers use this to route events to your `addHandler()` callbacks. `setNotifications()` is a higher-level API that uses both layers internally. You can use either layer directly. --- ## Attribute-decorated event handlers \{#attribute-handlers} Any function in the policy document decorated with a meter event attribute fires automatically when that event type occurs: ```typescript const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); policy.doc.parse(` #[meter-overage] fn handle_overage(event: obj) { // fires on every meter-overage event } #[meter-limit] fn handle_limit(event: obj) { // fires on every meter-limit event } #[meter-changed] fn handle_changed(event: obj) { // fires on every meter-changed event } `); ``` Multiple functions can have the same attribute — all fire. The firing order follows document order. Functions can also be zero-argument: ```stof #[meter-overage] fn log_overage() { // fires on overage, no event data needed } ``` --- ## Registering TypeScript functions callable from Stof \{#doc-lib} `policy.doc.lib()` registers a TypeScript function under a namespace, making it callable from Stof using the `?Namespace.function()` optional-call syntax: ```typescript policy.doc.lib('Alerts', 'notify', (customerId: string, entitlement: string, remaining: number) => { slack.send('#alerts', `${customerId} is low on ${entitlement}: ${remaining} remaining`); }); policy.doc.parse(` #[meter-changed] fn watch_usage(event: obj) { if (event.remaining < 1000) { ?Alerts.notify(event.customer.id, event.entitlement, event.remaining); } } `); ``` The `?` prefix is Stof's optional-call operator — a no-op if `Alerts.notify` isn't registered rather than throwing. Safe to use when the library may not always be present. :::tip[Async support] Stof supports async natively. Async TypeScript functions registered with `doc.lib()` are supported and can be awaited from within Stof if needed. ::: ### Passing objects to TypeScript TypeScript functions receive Stof values as arguments. To pass an object, serialize it to JSON first using `stringify('json', event)` — a Stof built-in: ```typescript policy.doc.lib('App', 'meter_overage', (json: string) => { const event = JSON.parse(json); billing.queueCharge(event.customer.id, event.entitlement, event.overage); }); policy.doc.parse(` #[meter-overage] fn on_overage(event: obj) { ?App.meter_overage(stringify('json', event)); } `); ``` ### Multiple namespaces ```typescript policy.doc.lib('App', 'meter_overage', (json: string) => { ... }); policy.doc.lib('App', 'meter_limit', (json: string) => { ... }); policy.doc.lib('Billing', 'queue_charge', (customerId: string, units: number) => { ... }); policy.doc.lib('Slack', 'send', (channel: string, msg: string) => { ... }); ``` --- ## The `App` namespace convention \{#app-namespace} The Limitr engine's built-in event system uses the `App` namespace to route events to your `addHandler()` callbacks. When you call `addHandler()`, it registers `App.event_handler` internally: ```typescript // This is what addHandler() does internally policy.doc.lib('App', 'event_handler', (key: string, value: unknown) => { for (const handler of eventHandlers.values()) handler(key, value); }); ``` The built-in `AppEvents` functions call `?App.event_handler(key, json)` — if `App` is registered, events route to your handlers. If it isn't, the call is a no-op. You can register additional `App` functions alongside `addHandler()` without conflict: ```typescript // addHandler() sets up App.event_handler policy.addHandler('billing', (key, value) => { ... }); // Register additional App functions for direct Stof calls policy.doc.lib('App', 'meter_overage', (json: string) => { // fires directly from doc.parse() Stof, bypassing addHandler() const event = JSON.parse(json); billing.directCharge(event.customer.id); }); ``` --- ## Complete example \{#example} Combining `doc.lib()`, `doc.parse()`, and `addHandler()`: ```typescript const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); // Standard App bridge — routes all events to addHandler() policy.addHandler('main', (key: string, value: unknown) => { if (key === 'meter-overage') { const event = JSON.parse(value as string); console.log('Overage:', event.customer.id, 'remaining:', event.remaining); } }); // Custom namespace with a specific handler policy.doc.lib('Custom', 'example_event_handler', (userId: string, remaining: number) => { console.log('Custom handler fired for', userId, 'remaining:', remaining); }); // Attribute-decorated handler that calls the custom function directly policy.doc.parse(` #[meter-overage] fn meter_over_limit(event: obj) { ?Custom.example_event_handler(event.customer.id, event.remaining); } `); await policy.createCustomer('user_growth', 'growth'); await policy.allow('user_growth', 'chat_input', 2_100_000); // triggers overage ``` When `meter-overage` fires, both paths run: 1. The built-in `AppEvents` handler calls `?App.event_handler('meter-overage', json)` → routes to `addHandler('main')` 2. The `#[meter-overage]` function fires → calls `?Custom.example_event_handler(userId, remaining)` Both paths run for every matching event. Order between attribute-decorated functions and `App` routing is not guaranteed. --- ## When to use each approach \{#choosing} | Approach | Use when | |---|---| | `addHandler()` | You want TypeScript-side filtering and handling. The standard approach for billing, alerting, and operational event handling. | | `setNotifications()` | You want conditions defined in the policy, changeable without code deploys in Cloud. The right tool for threshold-based alerts. | | `#[attribute]` + `doc.parse()` | You want event handlers embedded directly in the document with no abstraction. Useful for routing to custom library functions with specific argument shapes. | | `doc.lib()` | You need Stof code to call specific TypeScript functions with typed arguments, not just the generic `(key, json)` shape of `addHandler()`. | These approaches are additive — use as many as your application needs. A common production setup uses all four: `addHandler()` for billing and persistence, `setNotifications()` for Cloud-routed alerting, `doc.lib()` for domain-specific TypeScript functions, and `#[attribute]` handlers for logic that belongs in the document itself. --- ## Writing Policies in Stof Using Stof to author advanced policies. YAML, JSON, and TOML are fine for defining credits, plans, and limits. They stop working well the moment your policy needs to do something: embed an event handler, define a notification condition, add a capability, or express logic that doesn't fit a static key-value structure. [Stof](https://stof.dev) is the native format of the Limitr policy engine. Everything the engine does internally is Stof. When you write in YAML, it gets parsed into Stof at load time. Writing directly in Stof skips that translation step and gives you the full expressive power of the format — functions, type annotations, attributes, inline logic — without fighting the constraints of a data serialization format. :::note[Stof is optional] Stof is not required to use Limitr. For advanced use cases it enables you to enforce rules and conditions of arbitrary complexity — but most integrations start with YAML and only reach for Stof when they need it. If you're using Limitr Cloud, the dashboard handles policy authoring and you rarely need to write Stof directly. ::: :::tip[Try it] Use the [online playground](https://play.stof.dev/) to try Stof for yourself. Limitr may be your introduction to the project, but Stof is a general-purpose data runtime you can use anywhere. ::: --- ## The same policy in YAML and Stof \{#comparison} Here's a minimal policy in YAML: ```yaml policy: credits: token: description: AI token overhead_cost: 0.000003 pricing_model: flat price: amount: 0.000004 stof_units: int resets: true plans: starter: label: Starter default: true entitlements: chat_access: description: Access to AI chat chat_input: limit: credit: token mode: hard value: 500000 resets: true reset_inc: 1day ``` The same policy in Stof: ```stof policy: { credits: { token: { description: 'AI token' overhead_cost: 0.000003 pricing_model: 'flat' price: { amount: 0.000004 } stof_units: 'int' resets: true } } plans: { starter: { label: 'Starter' default: true entitlements: { chat_access: { description: 'Access to AI chat' } chat_input: { limit: { credit: 'token' mode: 'hard' value: 500000 resets: true reset_inc: 1day } } } } } } ``` Structurally nearly identical at this level. The difference becomes apparent when you add behavior. --- ## Stof syntax basics \{#syntax} :::tip[Full docs] See the [Stof docs](https://docs.stof.dev) for the complete language reference. ::: **Fields** — unquoted key-value pairs, no commas required: ```stof name: 'Starter' value: 500000 resets: true ``` **Objects** — curly braces: ```stof price: { amount: 0.000004 } ``` **Strings** — single or double quotes. Field names and type names are unquoted: ```stof description: 'Input tokens for Claude Sonnet 4' credit: 'token' ``` **Numbers and units** — duration and storage units are first-class Stof types: ```stof reset_inc: 1day value: 2GiB overhead_cost: 0.000003 ``` **Comments:** ```stof // This plan is for evaluation customers only hidden: true ``` **Functions:** ```stof fn matches(type: str, event: obj) -> bool { type == 'meter-changed' && event.remaining < 1000 } ``` **Type annotations:** ```stof str description: 'AI token' float overhead_cost: 0.000003 bool resets: true ``` **Attributes** — bracketed decorators placed before a field or function: ```stof #[meter-overage] fn handle_overage(event: obj) { // called on every meter-overage event } ``` --- ## What Stof gives you that YAML doesn't \{#advantages} ### Inline notification conditions In YAML, notifications must be loaded separately via `setNotifications()`. In Stof, they live directly in the policy file: ```stof policy: { credits: { ... } plans: { ... } notifications: { approaching-limit: { fn matches(type: str, event: obj) -> bool { type == 'meter-changed' && event.entitlement == 'chat_input' && event.remaining < (event.meter.limit * 0.2) } } hard-limit-hit: { fn matches(type: str, event: obj) -> bool { type == 'meter-limit' } } } } ``` These are indistinguishable from notifications loaded via `setNotifications()` at runtime — same behavior, same `addHandler()` integration — but they're part of the versioned policy file. ### Inline event handlers with attributes Attribute-decorated functions fire automatically on matching events. No `setNotifications()`, no `addHandler()` — the handler is embedded in the document: ```stof policy: { credits: { ... } plans: { ... } #[meter-overage] fn on_overage(event: obj) { // Call a TypeScript-registered library function ?App.meter_overage(stringify('json', event)); } #[meter-limit] fn on_limit(event: obj) { ?App.meter_limit(stringify('json', event)); } } ``` See [Embedded Event Handlers](./eventhandlers) for the full treatment of this pattern. ### Inline capabilities ```stof policy: { credits: { ... } plans: { ... } capabilities: { area: { version: 0.1.0 description: 'Calculate the area of a shape' parameters: [ { name: 'width', description: 'Width in any unit', schema_type: 'string' } { name: 'height', description: 'Height in any unit', schema_type: 'string' } ] result: 'area' #[run] fn calculate() { self.output.area = (self.input.width as float * self.input.height as float).round(2); } } } } ``` ### Type annotations Stof lets you annotate field types in the policy. The validator uses these at load time: ```stof credits: { token: { str! description: 'AI input token' // required (never null) string float overhead_cost: 0.000003 bool resets: true } } ``` --- ## Loading a Stof policy \{#loading} ```typescript // From a .stof file const policy = await Limitr.new(readFileSync('./policy.stof', 'utf-8')); // Inline (format defaults to 'stof' when omitted) const policy = await Limitr.new(` policy: { credits: { token: { overhead_cost: 0.000003 } } } `); // Empty policy const policy = await Limitr.new(); ``` --- ## A complete policy in Stof \{#complete-example} The AI token metering pattern, written natively in Stof with inline notifications: ```stof policy: { credits: { sonnet_input: { description: 'Claude Sonnet 4 input tokens' overhead_cost: 0.000003 pricing_model: 'flat' price: { amount: 0.000004 } stof_units: 'int' resets: true } sonnet_output: { description: 'Claude Sonnet 4 output tokens' overhead_cost: 0.000015 pricing_model: 'flat' price: { amount: 0.00002 } stof_units: 'int' resets: true } ai_credit: { description: 'AI Credits' label: 'AI Credit' unit: 'credit' } } exchange: { rune: { value: 1, currency: 'usd' } ai_credit: { value: 1.25, currency: 'rune' } sonnet_input: { value: 0.000004, currency: 'ai_credit' } sonnet_output: { value: 0.00002, currency: 'ai_credit' } } plans: { starter: { label: 'Starter' default: true entitlements: { chat_access: { description: 'Access to AI chat' } chat_input: { limit: { credit: 'sonnet_input', mode: 'hard', value: 500000, resets: true, reset_inc: 1day } } chat_output: { limit: { credit: 'sonnet_output', mode: 'hard', value: 200000, resets: true, reset_inc: 1day } } } } growth: { label: 'Growth' entitlements: { chat_access: { description: 'Access to AI chat' } chat_input: { limit: { credit: 'sonnet_input', mode: 'soft', value: 2000000, resets: true, reset_inc: 1day } } chat_output: { limit: { credit: 'sonnet_output', mode: 'soft', value: 800000, resets: true, reset_inc: 1day } } } topups: { monthly_credits: { description: '50 AI credits included monthly' credit: 'ai_credit' value: 50 included: true resets: true reset_inc: 30days reset_mode: 'hard' } } } } // Notifications inline — no setNotifications() call needed notifications: { approaching-limit: { fn matches(type: str, event: obj) -> bool { type == 'meter-changed' && event.entitlement == 'chat_input' && event.remaining < (event.meter.limit * 0.2) } } overage-started: { fn matches(type: str, event: obj) -> bool { type == 'meter-overage' && event.remaining >= -1 && event.remaining < 0 } } } } ``` --- ## Augmenting an existing policy with `doc.parse()` \{#augmenting} You don't have to write the entire policy in Stof to use Stof features. If your base policy is YAML, you can parse additional Stof into the document at runtime: ```typescript const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); // Add Stof-only features after loading the YAML base policy.doc.parse(` #[meter-overage] fn on_overage(event: obj) { ?App.meter_overage(stringify('json', event)); } `); ``` `doc.parse()` merges the Stof into the existing document — fields are added or replaced, existing fields not mentioned are untouched. This is how `setNotifications()` works internally. --- ## When to use Stof vs. YAML \{#when-to-use} | | YAML / JSON / TOML | Stof | |---|---|---| | **Credits, plans, limits, exchange** | ✓ | ✓ | | **Inline notifications** | ✗ | ✓ | | **Embedded event handlers** | ✗ | ✓ | | **Capabilities** | ✗ | ✓ | | **Type annotations** | ✗ | ✓ | | **Logic and expressions** | ✗ | ✓ | | **Familiar to most developers** | ✓ | Requires learning | Start with YAML. Reach for Stof when you want notifications or event handlers embedded in the policy file, need type annotations for clarity, or are building tooling that generates policies programmatically. --- ## Notifications & Alerting Real-time alerts & workflows. The Notifications page has two tabs: **Alerts** — the live feed of everything that has fired — and **Subscriptions** — the definitions that control when alerts fire and where they go. Notifications are workspace-level. Every team member sees the same alert feed and subscription list. Changes take effect immediately — saving or deleting a subscription pushes the updated definitions to all connected SDK instances in real time. --- ## Alerts tab \{#alerts} The Alerts tab is your operational dashboard for everything that has fired. Alerts are created automatically whenever a subscription's conditions are met. ### Alert statuses Every alert starts as **New**. Opening an alert automatically marks it **Seen**. Click **Resolve** to mark it done. Use the filter bar to view New, Seen, Resolved, or All alerts. The filter defaults to **New** — the unread queue. Use this as your daily triage view. ### Alert detail Click any alert row to open its detail panel. **Real-time alerts** show the credit name and description, entitlement name and plan, previous and current meter values, limit, remaining and overage where applicable, the attempted value for hard limit hits, a visual progress bar showing % of limit consumed, and the associated customer. **Group invoice alerts** show the invoice period, total amount and currency, margin for the period, and the customer the invoice is for. All alerts include a collapsible **Raw Event Data** section with the complete JSON payload — useful when triaging unusual events or building integrations. ### Alert lifecycle Alerts persist until resolved — there's no automatic expiry. Resolved alerts remain visible under the Resolved filter for audit history. --- ## Subscriptions tab \{#subscriptions} Subscriptions define when alerts fire and what happens. Navigate to the **Subscriptions** tab and click **+ New Subscription** to create one. ### Name and description **Name** — Required. Shown in the alert feed and in email notifications. Use something that makes the trigger obvious: "Growth Customer Overage", "Invoice Created — Enterprise", "Hard Limit Hit". **Description** — Optional. Shown in email notifications sent to your team. Use this to add context that helps the recipient act on the alert without opening the dashboard. ### Local Event Name Optional. A developer-facing identifier for this notification. When set, developers can subscribe to this notification by name in the SDK: ```typescript policy.addHandler('my-handler', (key, value) => { if (key === 'usage_limit_warning') { // fires when this Cloud notification matches const event = JSON.parse(value as string); yourApp.handleLimitWarning(event); } }); ``` Must be unique across all subscriptions in your workspace. No spaces, cannot start with a number. This is the bridge between Cloud-managed alerting and your in-process SDK handlers — the same event fires both the email action and your `addHandler()` callback. ### Enabled toggle Disabled subscriptions don't fire and don't create alerts. Use this to pause a subscription during an incident or while testing without deleting it. --- ## Trigger types \{#triggers} ### Group Invoice Created Fires when a group invoice closes at the end of a billing period. Use this for notifying your finance team, triggering billing workflows, or alerting on high-value invoices. This trigger has no event type or segment filter — it fires for all customers when their invoice period closes. ### Real-Time Event Fires immediately when a meter event occurs, evaluated in-process against your running policy. Use this for limit hits, overage alerts, approaching-limit warnings, or any condition that needs a real-time response. | Event Type | When it fires | |---|---| | Meter Overage | A soft limit has been exceeded and overage is being incurred | | Meter Reset | A meter has reset at the end of its reset window | | Meter Changed | Any successful enforcement call that updates a meter | | Hard Limit Hit | A hard limit has blocked a request | Leave the event type blank to match all four. --- ## Scoping real-time notifications \{#scoping} Real-time notifications have three independent layers of filtering. All three must pass for a notification to fire. ### 1. Event type filter The coarsest filter. Select which event type this subscription listens to, or leave blank to catch all. ### 2. Customer segment Optional. Select a saved segment to scope the notification to a subset of customers. The segment condition is evaluated against the customer who triggered the event. Example: alert on overage, but only for Growth and Enterprise customers. Create a segment for `customer.plan == "growth" || customer.plan == "enterprise"` and attach it to the notification. Free-tier customers triggering overage will be silently ignored. See [Managing Customer Segments](./segments) for how to define segments. ### 3. Event Condition (Stof) Optional. An additional Stof expression that filters by event-specific data. Expand the **Event Condition** section to add one — the editor validates syntax in real time. Four variables are available: **`event`** — The full meter event payload: `event.meter.value`, `event.meter.limit`, `event.meter.old`, `event.overage`, `event.remaining`, `event.entitlement`, `event.plan`. **`customer`** — The customer object: `customer.id`, `customer.plan`, `customer.type`, `customer.metadata`. **`policy`** — The live Limitr policy API. Call `?policy.value(customer.id, name)`, `?policy.limit(customer.id, name)`, and other SDK methods. **`data`** — Shorthand for `customer.metadata`. The last expression is the return value — truthy means the notification fires. **Only fire when a specific entitlement first crosses 90%:** ```stof event.entitlement == "chat_input" && event.meter.old < (event.meter.limit * 0.9) && event.meter.value >= (event.meter.limit * 0.9) ``` **Only fire for overages above a minimum amount:** ```stof event.overage != null && event.overage > 10000 ``` **Fire when storage first crosses 1 GiB:** ```stof event.meter.old < 1073741824 && event.meter.value >= 1073741824 ``` **Only fire during business hours (UTC):** ```stof const hour = Time.now().hour(); hour >= 9 && hour < 17 ``` --- ## Email action \{#email} Toggle **Email Action** on to send an email when this notification fires. **Product Members** — Sends to all members of your workspace. The default for operational alerts where the whole team should be aware. **Custom Addresses** — Sends to a specific list of email addresses. Press Enter or comma to add each address. Accepts any email address including PagerDuty routing addresses, team aliases, and on-call hooks. The description field appears in the email body. Use it to give recipients enough context to act without opening the dashboard. --- ## Common subscription patterns \{#patterns} ### Overage alert for high-value customers - **Trigger:** Real-Time Event → Meter Overage - **Segment:** Growth and Enterprise customers - **Email:** Product Members ### Hard limit hit — handled in SDK - **Trigger:** Real-Time Event → Hard Limit Hit - **Segment:** (all customers) - **Local Event Name:** `hard_limit_hit` - **Email:** (none) ### Approaching limit warning (80%) - **Trigger:** Real-Time Event → Meter Changed - **Event Condition:** ```stof event.meter.value >= (event.meter.limit * 0.8) && event.meter.old < (event.meter.limit * 0.8) ``` - **Email:** Product Members ### Invoice created notification - **Trigger:** Group Invoice Created - **Email:** Custom Addresses → finance@yourcompany.com - **Description:** A new billing period has closed. Review the invoice in the Limitr dashboard. ### Meter reset alert for Enterprise customers - **Trigger:** Real-Time Event → Meter Reset - **Segment:** Enterprise customers - **Local Event Name:** `enterprise_meter_reset` - **Email:** (none — handled in SDK) --- ## Cloud Quick Start Get started with Limitr Cloud. Limitr Cloud connects the enforcement engine you already understand to a managed layer that handles policy versioning, customer state, live per-customer margin, usage-based invoicing, and team alerting. The integration is a one-line change. Everything else stays the same. :::note[Prerequisites] A Limitr Cloud account and an API token from the [dashboard](https://cloud.limitr.dev). Familiarity with the [Quick Start](../../spec/quickstart) is assumed but not required. ::: --- ## Connect \{#connect} ```typescript const policy = await Limitr.cloud({ token: process.env.LIMITR_TOKEN }); ``` That's the swap. Your policy now lives in the Cloud dashboard — versioned, auditable, editable by any authorized team member without a deploy. `allow()`, `increment()`, `check()`, `remaining()` — every method works identically to the local engine. `Limitr.cloud()` returns `undefined` if the token is invalid or the connection fails. Always check for it: ```typescript const policy = await Limitr.cloud({ token: process.env.LIMITR_TOKEN }); if (!policy) throw new Error('Failed to connect to Limitr Cloud — check your token'); ``` --- ## What changes \{#what-changes} **Policy sync.** The WebSocket connection fetches your policy from the dashboard on startup and keeps it live. When you update a plan limit, add a tier, or change an overage rule in the dashboard, your running application reflects it immediately — no deploy, no restart. **Customer state sync.** The first time any enforcement method is called for a customer ID that doesn't exist locally, the SDK fetches their state from Cloud before proceeding. For new customers, use `ensureCustomer()` — it's a no-op if the customer already exists locally or in Cloud. **Enforcement stays in-process.** The WebSocket carries policy updates and customer state — it is not on the hot path. `allow()` is still sub-millisecond. --- ## What you get \{#what-you-get} ### Policy management without deploys Every change made in the Cloud dashboard takes effect in your running application within seconds. Changes are versioned and auditable — you can see who changed what and when, and roll back immediately if something goes wrong. To pin to a specific policy version rather than always receiving the latest (rarely recommended): ```typescript const policy = await Limitr.cloud({ token: process.env.LIMITR_TOKEN, policy: 'pol_abc123', // specific version ID from the dashboard }); // omit 'policy' (or use 'active') to always receive the latest published version ``` ### Live per-customer margin Because your credits carry `overhead_cost` and `price`, Limitr Cloud surfaces cost-to-serve versus captured revenue per customer in real time. Open the dashboard and see which customers are margin-positive, which are margin-negative, and which are about to exhaust a soft limit — before the billing period closes. ### Usage-based invoicing Invoices are generated directly from metered consumption. What customers used is what they owe. Cloud connects to your billing provider and handles invoice generation from the policy's metered state. ### Team alerting Notification conditions defined in your policy route automatically to Slack or email. A customer crossing 80% of their limit, a hard limit firing on a key account, a soft limit going into overage — define the condition once in the policy, and Cloud routes it to the right team without additional application code. --- ## `denyUnconnected` \{#deny-unconnected} When the WebSocket connection drops — network hiccup, Cloud maintenance, deployment restart — `denyUnconnected` controls what `allow()` returns for customers whose state isn't locally available. ```typescript const policy = await Limitr.cloud({ token: process.env.LIMITR_TOKEN, denyUnconnected: true, // default }); ``` | Value | Behavior | |---|---| | `true` (default) | `allow()` returns `false` when disconnected. Conservative — you never over-serve. | | `false` | `allow()` works normally when disconnected; remote sync is queued and flushed on reconnect. | :::tip[Stick with the default] For most applications, when disconnected from Cloud there are larger issues than just Limitr. The default (`true`) is recommended unless your product is intentionally offline-capable or runs in environments without a consistent connection. ::: The reconnect is automatic. Once the connection is restored, customer state re-syncs and enforcement resumes normally. --- ## Graceful shutdown \{#shutdown} `close()` flushes any pending state to Cloud before closing the WebSocket. Call it during your application's shutdown sequence. ```typescript process.on('SIGTERM', async () => { await policy.close(); process.exit(0); }); ``` Without `close()`, any metered usage that hasn't been flushed will be sent on the next reconnect. In most cases this is fine — the SDK queues unsent data. `close()` makes the flush synchronous and immediate. --- ## Full initialization options \{#options} ```typescript const policy = await Limitr.cloud({ token: string, // Required. API token from the dashboard. policy?: string, // Policy version ID. Default: 'active' (latest published). connectTimeout?: number, // ms to wait for initial connection. Default: 5000. denyUnconnected?: boolean, // Deny allow() when disconnected. Default: true. validate?: boolean, // Validate the policy on load. Default: true. wsAddress?: string, // Override WebSocket endpoint. Default: wss://api.limitr.dev. ticketAddress?: string, // Override auth ticket endpoint. Default: https://api.limitr.dev. }); ``` `wsAddress` and `ticketAddress` are for self-hosted or private Cloud deployments. Most users should never set these. --- ## From local to Cloud \{#migration} If you've followed the [Quick Start](../../spec/quickstart), the transition is: ```typescript // Before const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); // After — remove the file, remove the format arg, add the token const policy = await Limitr.cloud({ token: process.env.LIMITR_TOKEN }); if (!policy) throw new Error('Cloud connection failed'); // Everything below this line is identical await policy.ensureCustomer('user_abc', 'starter'); const allowed = await policy.allow('user_abc', 'chat_input', 4200); const remaining = await policy.remaining('user_abc', 'chat_input'); ``` Your policy file is no longer needed in your repository. Your plan limits, credit definitions, and overage rules now live in the dashboard and can be updated by anyone with access — without touching your codebase. --- ## Managing Customer Segments Dynamic, reusable filters that group customers together. Customer segments are dynamic, reusable filters that group customers by any combination of plan, type, metadata, usage, or margin. Once defined, a segment can be referenced by notifications (to scope alerts to specific customers) and by pricing rules (to apply different prices to a subset of customers without changing the plan). Segments are workspace-level — shared with all members of your team and applied to both test and live environments. --- ## What a segment is \{#what-is-a-segment} A segment is a named condition. At runtime, Limitr evaluates the condition against each customer object and the live policy to determine whether the customer matches. The condition is a Stof expression that returns a truthy value for matching customers. Three variables are available in every condition: **`customer`** — The full customer object: `customer.id`, `customer.plan`, `customer.type`, `customer.label`, `customer.meters`, `customer.grants`, `customer.metadata`, `customer.cloud_created`, `customer.cloud_updated`. **`policy`** — The active Limitr policy API. Call `?policy.entitlement(customer.id, name)`, `?policy.limit(customer.id, name)`, `?policy.value(customer.id, name)`, `?policy.customer_local_margin_breakdown(customer.id)`, and other policy methods directly from the condition. **`data`** — A shorthand alias for `customer.metadata`. Use `data.my_key` instead of `customer.metadata.my_key`. Conditions are evaluated per-customer whenever a matching event fires (for notifications) or when resolving the effective price for a billable action (for pricing rules). --- ## Creating a segment \{#creating} Navigate to **Customer Segments** in the dashboard and click **+ New Segment**. Give the segment a name and optional description, then define the condition. ### Simple mode A point-and-click condition builder. Add one or more conditions using these types: | Type | Description | |---|---| | **Plan** | Match customers on a specific plan. Selects from your policy's plan list. | | **Customer Type** | Match by customer type string: `user`, `org`, or any custom type. | | **Email** | Match by `customer.metadata.email`. Supports `==`, `!=`, `contains`, `starts_with`, `ends_with`. | | **Metadata Key** | Match by any key in `customer.metadata`. Supports all comparison operators. Values are auto-coerced to boolean, number, or string. | | **Created** | Match customers created before or after a specific date. | All conditions are combined with AND — a customer must satisfy every condition to match. Simple mode converts your conditions to Stof automatically. Switch to Advanced mode at any time to see and edit the generated expression. ### Advanced mode (Stof) A code editor for writing the condition directly in Stof. The editor validates syntax in real time — a green **✓ valid** indicator means the expression parses correctly. The last expression in the condition is the return value; truthy means the customer matches. ```stof customer.plan == "growth" ``` ```stof customer.plan == "enterprise" && customer.type == "org" ``` ```stof // Match customers with a specific metadata field data.internal_tier == "pilot" ``` Advanced mode is required for conditions involving policy calls, meter state, or multi-step logic. --- ## Templates \{#templates} The **From template…** dropdown provides six ready-to-use conditions that cover the most common operational use cases. Applying a template switches to Advanced mode with the condition pre-filled. ### Recently Created Customers created within the last 7 days. ```stof const ts: ms = Time.now() - 7days; customer.cloud_created != null && customer.cloud_created > ts ``` ### Active Customers Customers updated within the last 30 days. ```stof const ts: ms = Time.now() - 30days; customer.cloud_updated != null && customer.cloud_updated >= ts ``` ### Inactive Customers Customers not updated in the last 30 days. ```stof const ts: ms = Time.now() - 30days; customer.cloud_updated != null && customer.cloud_updated < ts ``` ### Overage Customers with at least one entitlement where the meter has exceeded a soft limit. ```stof for (const field in customer.meters.fields()) { const name = field[0]; const ent = ?policy.entitlement(customer.id, name); if (ent != null && ent.limit != null && ent.limit.mode == 'soft') { const limit = ?policy.limit(customer.id, name); const current = ?policy.value(customer.id, name); if (limit != null && current != null && current > limit) return true; } } false ``` ### Hard Limit Imminent Customers with a hard-limited entitlement at 90% or more of its limit. ```stof for (const field in customer.meters.fields()) { const name = field[0]; const ent = ?policy.entitlement(customer.id, name); if (ent != null && ent.limit != null && ent.limit.mode == 'hard') { const limit = ?policy.limit(customer.id, name); const current = ?policy.value(customer.id, name); if (limit != null && current != null && current >= limit * 0.9) return true; } } false ``` ### Negative Snapshot Margin Customers with a negative margin in the current snapshot. ```stof const snapshot = ?policy.customer_local_margin_breakdown(customer.id); (?snapshot.get('margin') ?? 0) < 0 ``` --- ## Custom condition examples \{#examples} ### Customers on Growth or Enterprise ```stof customer.plan == "growth" || customer.plan == "enterprise" ``` ### Org customers only ```stof customer.type == "org" ``` ### Enterprise orgs with more than 20 seats used ```stof customer.type == "org" && customer.plan == "enterprise" && (?policy.value(customer.id, "seats") ?? 0) > 20 ``` ### Customers with a specific metadata tag ```stof data.billing_tier == "custom" || data.account_manager != null ``` ### Customers with active grants ```stof customer.grants != null && customer.grants.fields().len() > 0 ``` ### High-value customers approaching their limit ```stof for (const field in customer.meters.fields()) { const name = field[0]; const ent = ?policy.entitlement(customer.id, name); if (ent != null && ent.limit != null && ent.limit.mode == 'hard') { const pct = ?policy.value(customer.id, name, true); if (pct != null && pct >= 80) return true; } } false ``` --- ## Using segments in notifications \{#notifications} When creating a notification, you can scope it to a segment. The notification only fires for customers that match the segment condition at the time the event occurs. For example: a `meter-overage` notification that should only alert your team when a Growth or Enterprise customer goes into overage — not every free-tier user. Create a segment for `customer.plan == "growth" || customer.plan == "enterprise"` and attach it to the notification. Segment evaluation in notifications is real-time. A customer who upgrades from Starter to Growth immediately starts matching a Growth segment — no re-sync required. --- ## Using segments in pricing rules \{#pricing-rules} Pricing rules let you apply a different price to a segment of customers without changing the plan. The rule specifies a segment and a set of price overrides keyed by target (`credit:name`, `topup:name`). When Limitr resolves the effective price for a billable action, it evaluates segment conditions in priority order. The first matching rule's overrides apply. If `supersedes: 'customer-overrides'` is set on the rule, it takes precedence even over per-customer price overrides. Pricing rules are configured in the dashboard under your policy settings. Define the segment first, then reference it in the rule. --- ## Setting an active segment \{#active-segment} On the Segments page, each segment has a **Set Active** button. The active segment filters the customer view across the dashboard — the customer list, margin views, and usage tables all apply it as a filter. This is how you scope your operational view to a specific group: "show me only Growth customers in overage" or "show me only customers created this week." Only one segment can be active at a time. Click **Set Active** on a different segment to switch, or click the active segment's button again to clear it. --- ## Editing and deleting segments \{#editing} Click **Edit** on any segment row to modify its name, description, or condition. Changes take effect immediately — notifications and pricing rules referencing the segment use the updated condition on the next evaluation. :::warning[Downstream effects] Changing a segment condition affects all notifications and pricing rules that reference it. Review those before saving a condition change. ::: Click **Delete** to remove a segment. Notifications and pricing rules that reference it by ID are not automatically updated — update those separately before deleting the segment. --- ## Homepage Limitr is an embedded usage runtime for AI products: it enforces what every user and agent can do, how much they get, and what it costs them, running in-process as WebAssembly rather than as a remote API call. This document consolidates the content of Limitr's four marketing pages — the homepage and the Monetize, Control, and Analyze pages — since those pages are built as standalone Docusaurus `src/pages` (custom React components with MDX-supplied copy), not docs, and are not otherwise included in generated documentation or llms.txt output. --- # Homepage ## Positioning **Eyebrow:** The usage runtime for AI products **Headline:** AI becomes profitable when usage is visible and controlled. **Subhead:** Limitr is an embedded runtime that enforces what every user and agent can do, how much they get, and what it costs them, giving you control over cost-to-serve and revenue capture. ## Why now **"What changed?"** — Margin is no longer fixed. It moves with every call. ## Solutions that Limitr Offers to Customers ### Usage Monetization **"Claim"** - Turn usage into revenue. **"Description"** - Any pricing model, versioned & auditable, as a single managed config. Define entitlements, limits, credits, & prices — Limitr meters and charges every call against it, per customer, per plan, live. ### Usage Control **"Claim"** - Gate access. Cap usage. Control overhead. **"Description"** - Usage limits, access gates, and spend caps enforced where calls happen, translated into your desired units — per customer, per agent, per pipeline, per vendor, embedded & real-time. ### Usage Analytics **"Claim"** - Live margin and clear attribution. **"Description"** - Revenue, cost, and usage data captured at the same moment, with clear vendor, customer, and feature attribution and analytics — never a stale monthly export from three different tools. ## Differentiators ### Actually embedded Limitr isn't a remote API — it's a WebAssembly runtime, running inside your own process. The decision happens where your code already lives (works offline, too). ### Policy-as-document approach Pricing, limits, and entitlements live in one versioned, managed, and open-source document, not scattered across servers, code, and spreadsheets. ### Translate and exchange value Limitr exchanges and translates usage — tokens, seconds, credits — into what actually matters: outcomes, USD, any currency, live, across every service and vendor. ## FAQ — Start here **Q: What's the difference between a billing platform and a pricing runtime?** A billing platform records what happened and charges for it after the fact — invoices, payments, subscriptions, revenue recognition. A pricing runtime decides what's allowed before the action executes, and meters it in the same operation. If enforcement lives downstream of consumption, the cost is already incurred by the time anything reacts. Limitr runs at the moment of consumption, in-process, so a limit holds before you pay a vendor for the tokens rather than after. Keep Stripe, Maxio, QuickBooks, or your homegrown invoicing for collecting money. Limitr owns the decision layer above it: what each customer gets, what it costs you, what you charge, enforced live. We integrate with the billing platform and workflow of choice. **Q: I just want to control usage, do I also have to monetize it?** No. Each solution — monetize, control, analyze — can be implemented together or separately. Most teams start with analytics (you can't control what you can't see), then move into controlling usage and sometimes monetizing it (or portions of it). The usage you monetize may also not be the same raw usage that you control — most teams stack and layer entitlements to achieve their desired pricing & control models that work together (e.g. control tokens, charge only for successful runs). **Q: Is Limitr only for AI companies?** No. AI is the loudest version of the problem, not the whole problem. Limitr meters any unit you can define: AI tokens by model, API calls, seats, vendor connections, SMS messages, avatar minutes, GPU seconds, storage, agent runs, outcomes, or a composite credit of your own design. In practice our customers split roughly evenly. Some are metering multi-model AI pipelines. Others are legacy SaaS companies moving off flat subscriptions — per-seat billing that keeps drifting out of sync, text-message overages reconciled by hand, or a shift from a complicated spend-based calculator to a clean per-unit price. Same infrastructure problem, no AI involved. **Q: Does Limitr just track usage, or does it actually enforce and bill it?** All three. Plenty of tools will tell you what your customers consumed after the fact. Limitr enforces the limit at the moment of consumption, meters usage in that same atomic operation, and feeds the result straight into invoicing. Limits run in three modes, switchable without a code change: hard blocks the action at the cap, soft allows the overage and optionally bills on it, and observe tracks everything while enforcing nothing. Most teams start in observe, learn what their usage actually looks like, then turn on enforcement once the data supports the decision. ## FAQ — For engineering **Q: Will this add latency to my product?** No. The policy runs in-process as WebAssembly, colocated with your application. Every allow(), check(), and increment() executes locally in microseconds, with no network call on the hot path — compared with the 50 to 100+ milliseconds typical of enforcement systems that make a remote API call per decision. Usage events sync to Limitr Cloud asynchronously over a background WebSocket, and policy updates push down the same way. The hot path stays fast; your data stays current. **Q: How much code is this, and what languages do you support?** The integration surface is deliberately small. You call ensureCustomer at login or first touch, then policy.allow at the point of consumption with an entitlement name and an amount. That's the core of it. Engineering wires up the entitlement by name and passes in usage quantities (optionally gating access, too). Everything behind that name — limits, prices, tiers, per-customer overrides — is configuration that changes without touching your code. The SDKs are each a thin wrapper over the same core WASM module, so additional languages take days rather than quarters to support. TypeScript/JS is most supported today, but tell us what you're running and we'll confirm timing before you commit to anything. **Q: Is Limitr like an AI gateway?** Dynamic model switching is a core part of AI usage control, and we are very good at that. We are also very good at putting prices to usage, so you can track and control margin-to-serve in the ways that matter, not just a general aggregate cost-to-serve which leaves a lot of blind spots. Limitr does not, however, make LLM requests on your behalf or repackage specific vendor tokens. We do not provide a router for your AI requests — we provide a control plane around them. Limitr works with any AI vendor (including gateways). If you're looking for spend management alone, Limitr will suffice. If you're looking for a unified endpoint, routing, & other AI-specific guardrails, a gateway may also make sense in parallel. **Q: What happens if the connection drops, or if something gets counted wrong?** On disconnect, the runtime will try reconnecting automatically and gives you the option to continue operating normally offline or block all usage until back online (the default to preserve state). If your app is offline, you typically have larger issues to contend with. The default is configurable in the SDK, and enforcement never stops working. The open-source engine also runs fully offline against a local policy file, enabling you serve users in low or no-connectivity regions, or have a separate offline/backup usage policy for failure and testing modes of operation. For bad data: meters move in both directions. If a runaway process inflates a counter, decrement it through the SDK or set the value directly in the dashboard. Counting happens locally and serializes to the server, where operations are atomic, so you won't double-count. **Q: How does the open-source engine relate to Limitr Cloud?** Same core. The open-source enforcement engine runs in-process, is fully self-hostable, and is free permanently. Cloud adds the managed policy dashboard, versioning and instant rollback, per-customer analytics and margin data, usage-based invoicing, live alerting, and the Stripe/billing integrations. The enforcement API is identical either way, so moving from local to Cloud is a one-line change. Prototype against the open source engine without talking to us first — several teams have. **Q: Does the policy-as-config have validation?** Yes, our open-source runtime built on Stof contains validation rules, and can literally validate itself so that you never have an invalid policy. Inside the npm.js package, Limitr.new(..) actually calls validate by default and will throw an error if the provided policy does not fit the spec, providing a message for where the error resides. If you're using Limitr Cloud, the policy is fully managed and will always be validated when changes are made and before it gets used within your app or service. ## FAQ — For finance and product **Q: Can non-engineers change limits and prices without a deploy?** Yes, and it's the main reason teams buy this. Anyone with dashboard access and permission can change a limit, add a tier, adjust an overage rule, apply a discount, or gate a feature. Changes publish live and propagate to every connected service instantly — no PR, no deploy, no release coordination. Per-customer and per-segment pricing works through rules rather than proliferating plans. Keep a small number of plans, then layer overrides on top: replace a price outright or apply a percentage in either direction, with an expiry date and an approval note attached. Sales can structure a custom deal without engineering in the loop. Every change is versioned with full history and reverts instantly. **Q: Can't we just build this ourselves?** Honest answer: if your pricing model is static, you're just trying to monetize, and hardcoding it is fine, then you should. The case for Limitr is a function of how often your packaging changes and how expensive it is to extend, maintain, customize, and analyze. Transparently, though, almost nobody's model stays static, and once you dive in, this critical layer of your app gets large, messy, and nuanced, quickly. It adds up once you need to split billing across products, apply a discount to one specific tool call, burndown credit balances differently across accounts, run different budgets per model in the same pipeline, handle a mid-period upgrade without double-charging, or roll out a tier without a deploy. Together they become a permanent engineering surface that grows with every commercial decision the business makes, often brittle and expensive to maintain. The estimates we hear from technical buyers cluster around 1-3 engineer-months for a first version, plus indefinite maintenance and additional internal projects for observability. Instead, our contracts are priced for outsized savings on your end, and because the enforcement engine is open source, you aren't betting your pricing infrastructure on our roadmap. **Q: Our setup is complicated — multiple products, orgs and users, several vendors. And we haven't finalized our model yet. Is it too early?** Complexity is the use case. Customer objects are arbitrary: an org, a workspace, a user, an agent, a project, whatever you need to meter. They reference each other, so a shared meter like seats resolves to the right level automatically, and group invoices roll up from individual ones. Credits carry both what you pay a vendor and what you charge, per model and per vendor, so margin resolves per customer, per feature, and per provider without extra instrumentation. On timing: not too early. Observe mode exists precisely for this — meter everything, enforce nothing, and set your allotments from real data instead of a guess. Implement in stages over time, when and how it makes the most sense for your specific products. ## Closing statement The embedded runtime for profitable AI. --- # Monetize page ## Positioning **Eyebrow:** Monetize **Headline:** Scale your revenue. Monetize usage. **Subhead:** Any pricing model, versioned and auditable. Define entitlements, limits, credits, and prices — Limitr meters and charges every call against it, per customer, per plan, live. ## Why now **"What changed?"** — Every feature you ship now carries a cost that moves. Pricing has to move with it. ## Motivators — why teams monetize with Limitr ### Ship any pricing model — flat, seat, usage, or all of them at once. Most billing tools force a single model and make you rebuild when the market shifts. Limitr's policy is always hybrid and won't get in your way — flat fees, seats, usage meters, and credit pools live in the same document, combined however the plan actually needs. ### Move at market and development speed. Pricing and packaging live outside the codebase. A rate change, a new tier, an limit adjustment — versioned and published live the instant someone with permission makes it. No PR, no deploy, no release train to catch. ### Every enterprise deal, without a special build. Layer a custom rate, a bespoke limit, or a negotiated discount onto any customer or contract — with its own expiry and audit trail — without forking pricing logic or hardcoding a one-off exception. ### Meter, price, and invoice — accurate to the moment. Every call is accurately captured the instant it happens, not reconciled at month-end from logs and guesses. The customer invoice matches exactly what was metered, auditable and reportable in units that matter on both sides. ### Get ahead of usage before you or your customer feel it. Set thresholds once and the runtime watches continuously — a customer trending toward their cap, projected to cross a utilization target this week, whatever matters to your team. Alerts fire the moment it's crossed, in-app, configurable without waiting on engineering. ## Differentiators ### Policy, not code Flat fees, seats, usage, credits — every pricing primitive lives in one open-source, versioned policy document, not scattered across code and spreadsheets. That's what makes any model possible: there's nothing to hardcode. ### Runtime, not a request What only a runtime can do is hold state — enforcing one cap across an entire pipeline, every vendor call included, because the context never leaves your process. ### One exchange, real ROI The same exchange that meters usage also translates it into what the customer actually cares about — cost avoided, outcomes delivered, value earned. Their invoice maps to their ROI, so expansion conversations explain themselves. ## FAQ — Pricing & billing **Q: Can I really run flat, seat, usage, and credits in one plan, not just one model at a time?** Yes — that's the default, not a special configuration. A policy document can combine a flat platform fee, a per-seat charge, a metered usage rate, and a pooled credit balance in the same plan, applied to the same customer, at the same time. Most billing tools treat these as separate products bolted together, which is why switching models usually means a migration. Limitr treats them as primitives inside one document — you're not choosing between flat and usage, you're deciding which primitives this specific plan needs, and changing that mix later doesn't require re-platforming. **Q: How fast do pricing changes actually take effect once someone publishes them?** Immediately, in the literal sense — a published change propagates to every connected service the moment it's saved, because the policy represents the runtime directly, not a config that gets deployed somewhere downstream. There's no build step, no PR, no release window to wait for. The person changing the price and the person who set the deploy schedule for your application don't need to be the same team, or even know about each other's calendar. **Q: How do custom per-customer deals work in practice — is it really no special build?** A custom deal is an override layered on top of a base plan, not a fork of your pricing logic. You keep a small number of standard plans, then apply a different rate, limit, or discount to specific customer(s) or contract(s) — each override carries its own expiry and a record of who approved it. Practically: sales negotiates a deal, someone with permission applies the override, and it's live for that account without a code change or a conversation with engineering. Nothing about the base plan changes for anyone else. **Q: How accurate is the invoice — is "priced at the moment of consumption" literal?** Literal. Every call is metered and priced against the ledger it belongs to in the same operation it happens in — not batched, not reconciled later from logs. The number on the invoice is the number the ledger recorded at the instant of consumption. That matters most at the edges: usage right at a plan boundary, a burst right before a billing period closes, a customer who upgrades mid-cycle. Those are exactly the cases where systems that reconcile after the fact tend to drift from what actually happened — there's nothing to drift here, because there's no gap between metering and pricing to begin with. **Q: Can alerts be set up without engineering, and what can they actually trigger on?** Yes — thresholds are configured in the dashboard, not in code. You can set a rule against almost anything the runtime already tracks: a customer trending toward their cap, projected utilization over the next few days, a spend threshold crossed mid-period, a specific entitlement running low. Who gets notified is also configurable per rule — an internal Slack channel, a customer success queue, the customer themselves. The common pattern is proactive account management: knowing a customer is about to hit a wall three days before they do, instead of finding out when support gets the ticket. ## Closing statement The pricing engine that keeps up with your business. --- # Control page ## Positioning **Eyebrow:** Control **Headline:** Gate access. Cap spend. Control usage. **Subhead:** Limits, access gates, and spend caps enforced where calls happen, translated into units that matter — per customer, per agent, per pipeline, per vendor, embedded and real-time. ## Why now **"What changed?"** — Runaway AI doesn't send a warning. It sends an invoice. ## Motivators — why teams control usage with Limitr ### Ship AI features without betting the company on the bill. Start in observe mode — meter everything, enforce nothing, and watch real usage before committing to a number. Flip to soft limits when you're ready to charge for overage, then hard limits when you need a wall. Same policy, no rebuild, no re-launch. ### Stop overspend before it happens. Limits, credit balances, and rate governors — collapsed at once to provide a live generation allowance, like max_tokens, before call execution. A dynamic ceiling to support agreements and prevent sudden shutoffs. ### Enforce spend controls, no matter what's running underneath. Self-serve spend caps, overhead caps, observers, agent caps with scheduled resets — control spend in every situation, across services, vendors, and various credit exchange rates. ### Place access gates for every user and agent. Entitlements are gates with optional usage limits attached. They scope exactly what each tier, customer, or agent can do — defined as policy, enforced directly in-process where all the action happens. ## Differentiators ### One policy, every kind of limit Switching a mode or scoping access is one configuration change, not two systems. An entitlement without a limit is an access gate, add a limit to charge, control, and observe any type of usage — any strategy, one open policy spec. ### Enforced in-context, not outside The runtime never leaves your application context — calculate dynamic usage allowance, cap spend over a single pipeline run, or change behavior for an agent. The complexity of dynamic controls won't get in the way of shipping. ### Control units that matter Transform and roll-up disparate credit types into a single unit for spend caps, overhead control, and observability in context. Multi-vendor pipeline spend is unified, controlled, and compared on equal ground. ## FAQ — Limits & enforcement **Q: What's the actual difference between soft and hard limits?** Hard limits block the call outright once the cap is reached. Soft limits let the call through. Depending on how you configure soft limits in your policy, you can and just observe the overage usage, set a price and bill or invoice for the overage, or enable notifications to alert upon overage events. Observe mode meters everything and enforces nothing, which is where most teams start. The three aren't separate products; they're one policy with a mode field, so moving from observe to soft to hard is a simple change within the Limitr policy. **Q: How do spend caps relate to entitlements and limits?** Spend caps sit above and outside of policy-level entitlements and limits. Plans group entitlements, and entitlements can have usage limits tied to them that define included/overage thresholds. Caps sit outside so that they can span across many entitlements at once. This enables caps to be used for observing or enforcing spend per agent, pipeline, vendor, credit, or customer. Self-serve caps, overhead maximums, spend analysis — a versatile paradigm for all of your use cases. **Q: Can I scope access differently for different agents, or is it all-or-nothing?** Per-agent, per-customer, per-tier — however granular you need. A "customer" can be a user, account, workspace, agent, or anything else you'd like to limit, control, or analyze — facing either internally or externally. Practically speaking, controlling one of your own agent's usage limits is no different from controlling a user's usage, the user just might get an invoice at the end of the month for it. **Q: How can I handle errors and events?** Limitr is an embedded runtime that is entirely event-driven. Therefore, you always have access directly in-process to local events like meter changes or limits being hit. The runtime is flexible, so you can always add your own rules, tooling, and custom events. SDK-level handlers are also available for convenience, allowing your application to listen and handle the events it needs to in its native tongue. Explicit errors are also always provided. What you do with events and errors is up to you: show the user a limit message, queue a request, fall back to a cheaper model, whatever fits your product. ## Closing statement Usage that's never outside of your control. --- # Analyze page ## Positioning **Eyebrow:** Analyze **Headline:** Live margin. Clear attribution. Analyze usage. **Subhead:** Revenue, cost, and usage data captured at the same moment, with clear vendor, customer, and feature attribution — always live, reported the moment usage happens. ## Why now **"What changed?"** — Your best customer could be your worst deal. ## Motivators — why teams analyze usage with Limitr ### Catch margin erosion while it's still small. A drift in price or spike in usage caught this week can be a quick fix. Caught at quarter-end or after finance has already closed the books on it, and it's a larger problem. Margins that update with each call catches the difference while it's still small, and alerting ensures the correct team is informed, live. ### Know which customers are currently profitable. Revenue size and profitability aren't the same measurement, and they're changing minute by minute, not just month to month. See margin per customer as it happens so you know which accounts are quietly losing money. ### Prioritize features that are worth building further — not just popular. It's not enough to just know what's popular or how much you're spending on tokens. A heavily-used feature can be a margin sink if its cost outpaces what it's priced to capture. A worth-while determination is nuanced — make sure you have the right data to make decisions against. ### Know what each vendor relationship is worth. Running multiple models or vendors means cost-per-equivalent-outcome varies more than most teams realize. Minimize spend with dynamic model switching based on plan, usage, current metadata, and/or margin. Most teams seek cost-to-deliver AI, but are unable to answer margin-to-deliver outcomes — be one that can answer both. ## Differentiators ### One usage policy Monetize, Control, and Analyze aren't three separate systems — they're three views into the same policy document. You can't price what you can't measure, and you can't control what you can't see. ### One exchange, every dimension Currency and credit exchange in one mechanism, enables slicing margin by customer, feature, or vendor. Ask questions from any angle, and be confident that your comparisons are correct and meaningful — USD, tokens, seconds, credits — both backward and forward. ### Alerted, not just reported The runtime evaluates every call as it happens. Notify the right team the millisecond a threshold crosses — a margin gone negative, a sudden usage spike — instead of missing it in next week's report. ## FAQ — Margin & attribution **Q: Is the margin number real cost, or an estimate?** Each credit definition holds your price and an optional overhead cost. This is how margins are calculated in real-time per call while remaining flexible enough to capture custom or non-list prices. **Q: How current is per-customer profitability — actually live, or a nightly batch?** Live. Margin per customer reads from the same ledger that meters usage and prices it in real time — there's no separate batch job reconciling yesterday's data overnight, because there's no gap between when a call happens and when it's reflected in that customer's number. **Q: What if features aren't a clean, first-class concept in my product?** Features map to entitlements, and entitlements are however granular you define them — a feature, a specific model call, an internal workflow step, whatever boundary is actually meaningful in your product. There's no requirement that it map to a UI-visible feature flag or a product team's own taxonomy. Most teams start coarse — a handful of major capabilities — and split further only once the coarse view surfaces something worth investigating. You don't need a finished feature taxonomy before this becomes useful. Entitlements are also often stacked and used together for different purposes. The access gate, token meter, and outcome billed may all be separate entitlements used within the same operation, and the user may only get invoiced for outcomes or tokens, or not at all. **Q: Doesn't cost-per-outcome ignore that models perform differently, not just cost differently?** No — outcome is defined by you, not assumed to be a raw API call. If quality matters more than raw cost for a given use case, the outcome unit can be a resolved query, a passed eval, a customer-facing result — whatever the comparison should actually be measured against. The point isn't always pick the cheapest vendor. It's making the real tradeoff visible — cost, quality, and reliability side by side — instead of defaulting to whichever vendor was integrated first and never revisited. **Q: Do I need a separate integration for this, or does it read the same data as Monetize and Control?** Same data, same integration. Analytics isn't a separate pipeline bolted on afterward — it reads from the identical ledger that Monetize prices against and Control enforces against, because it's the same policy document underneath all three. Practically: if you're already metering usage for pricing or limits, the margin and attribution data is already there. There's nothing additional to instrument specifically for analytics. ## Closing statement Know what's profitable, not just what's popular. --- ## Spend Alerts How to subscribe to events and define custom notifications. --- ## Usage Attribution How to attribute usage and spend to agents, pipelines, customers, and more. --- ## Credit-Based # Credit-Based Monetization How to create user-facing credit models. --- ## Live Demo # Monetization Demo Interactive monetization demo, running live in your browser. The particle decision field simulates various types of usage happening over time. Each color represents an entitlement-credit pair. The particles that make it across the line are allowed (`allow(...)` -> `true`), and the usage they represent is monetized accordingly. :::note This is actually running Limitr (@formata/limitr within React) in your browser via WebAssembly. Controls are simplified for obvious reasons — they change the policy directly. Limitr Cloud works similarly — updates are streamed real-time to clients anytime your policy is updated. ::: ## Simulation export const demoKinds = [ { kind: 'tokens', label: 'AI Tokens', color: '#5b8dee', weight: 3, amountRange: [500, 4000] }, { kind: 'seats', label: 'Seats', color: '#9b7ede', weight: 1, amountRange: [-1, 3] }, { kind: 'outcomes', label: 'Outcomes', color: '#4dbbb0', weight: 2, amountRange: [1, 5] }, { kind: 'storage', label: 'Storage (MB)', color: '#c9a227', weight: 2, amountRange: [1, 10] }, ]; export const demoPolicy = ` policy: { credits: { token: { unit: 'token', price: { amount: 0.000004 }, overhead_cost: 0.000003, stof_units: 'int' } seat: { unit: 'seat', price: { amount: 5 }, overhead_cost: 0, stof_units: 'int' } outcome: { unit: 'outcome', price: { amount: 0.25 }, overhead_cost: 0.10, stof_units: 'int' } mb_storage: { unit: 'mb_storage', price: { amount: 0.003 }, overhead_cost: 0.001, stof_units: 'MB' } // Internal denial reporting denial: { stof_units: 'int' } } plans: { demo: { label: 'Demo Plan' entitlements: { tokens: { limit: { credit: 'token', mode: 'soft', value: 1000, minimum: 0, resets: true, reset_inc: 20s } } seats: { limit: { credit: 'seat', mode: 'hard', value: 10, minimum: 0, resets: true, reset_inc: 60s } } outcomes: { limit: { credit: 'outcome', mode: 'soft', value: 20, minimum: 0, resets: true, reset_inc: 10s } } storage: { limit: { credit: 'mb_storage', mode: 'soft', value: 5MiB, minimum: 0, resets: true, reset_inc: 60s } } denials: { limit: { credit: 'denial', mode: 'observe', resets: true, reset_inc: 1min } } } } } } #[main] fn main() { self.policy.create_customer('demo', 'demo'); // USD spend & overhead last 5 min observers - margin can be determined using both self.policy.add_customer_cap('demo', 0, id='usd_charged_observer', credit='usd', exchangeable=true, ignore_grants=true, overage_only=true, observe_only=true, overhead_cost=false, follow_decrements=false, resets=true, reset_inc=1min ); self.policy.add_customer_cap('demo', 0, id='usd_overhead_observer', credit='usd', exchangeable=true, ignore_grants=true, overage_only=false, observe_only=true, overhead_cost=true, follow_decrements=false, resets=true, reset_inc=1min ); } #[meter-limit] fn usage_limit_hit(event: obj) { // we're just graphing all denials over time, and the meter is set to reset every 5 min anyways on the customer, so keeping it easy here self.policy.allow('demo', 'denials', 1); } // Set credit helpers for interactivity // how much will the customer get charged? fn set_credit_price(credit: str, amount: float) { const c = self.policy.credits.get(credit); if (c != null) c.price.amount = amount; } // how much do we get charged in overhead to deliver a credit? fn set_credit_overhead(credit: str, amount: float) { const c = self.policy.credits.get(credit); if (c != null) c.overhead_cost = amount; } // Set entitlement helpers for interactivity // 'hard' will block all overage, 'soft' will allow & bill for overage fn set_entitlement_mode(entitlement: str, mode: str) { const ent = self.policy.plans.demo.entitlements.get(entitlement); if (ent != null) { ent.limit.mode = mode; } } // included value (not billed for) fn set_entitlement_value(entitlement: str, value: float) { const ent = self.policy.plans.demo.entitlements.get(entitlement); if (ent != null) { ent.limit.value = value; } } // how long until meter resets to 0 (resets overage) fn set_entitlement_reset_inc(entitlement: str, inc: float) { const ent = self.policy.plans.demo.entitlements.get(entitlement); if (ent != null) { ent.limit.reset_inc = inc; } }`; export function MonetizeFieldDemo() { const policyRef = useRef(undefined); const [ready, setReady] = useState(false); const onDecision = async (particle) => { const policy = policyRef.current; if (!policy) return false; try { return await policy.allow('demo', particle.kind, particle.amount); } catch { return false; } }; const getCurrentCustomerChargedUSD = async () => { const policy = policyRef.current; if (!policy) return null; /* export interface LimitrCap { id: string; credit: string; value: number; exchangeable: boolean; ignore_grants: boolean; overage_only: boolean; observe_only: boolean; overhead_cost: boolean; follow_decrements: boolean; scope: string[] | null; meter_value: number; created_on: number; started: number | null; resets: boolean; reset_inc: number | null; reset_sch: string | null; last_reset: number | null; expires_on: number | null; } */ const cap = await policy.customerCap('demo', 'usd_charged_observer'); if (cap) { return cap.meter_value; // number } return null; }; const getCurrentOverheadUSD = async () => { const policy = policyRef.current; if (!policy) return null; const cap = await policy.customerCap('demo', 'usd_overhead_observer'); if (cap) { return cap.meter_value; // number } return null; }; const margin = (charged, overhead) => charged > 0 ? Math.round((charged - overhead)/charged * 10000) / 100 : 0; const getCurrentTokens = async () => { const policy = policyRef.current; if (!policy) return null; return await policy.value('demo', 'tokens') ?? 0; }; const getCurrentSeats = async () => { const policy = policyRef.current; if (!policy) return null; return await policy.value('demo', 'seats') ?? 0; }; const getCurrentOutcomes = async () => { const policy = policyRef.current; if (!policy) return null; return await policy.value('demo', 'outcomes') ?? 0; }; const getCurrentStorage = async () => { const policy = policyRef.current; if (!policy) return null; return await policy.value('demo', 'storage') ?? 0; }; const getCurrentDenials = async () => { const policy = policyRef.current; if (!policy) return null; return await policy.value('demo', 'denials') ?? 0; }; // CONSOLIDATED from 8 per-credit wrapper functions (setTokenPrice, // setSeatPrice, ... setStorageOverhead) into 2 generic ones — those 8 // were each a thin wrapper around the same two already-generic Stof // functions (set_credit_price/set_credit_overhead), just with the // credit name hardcoded per function. Consolidating here so // UsageControls can drive all four kinds through one set of handlers // instead of needing kind-specific wiring — and so adding mode/ // value/reset didn't mean writing 12 MORE near-identical wrappers on // top of the existing 8. // // kind (the entitlement name, e.g. 'tokens') and credit (e.g. // 'token') aren't the same string for every kind — confirmed against // the actual policy text before writing this, not assumed. const CREDIT_BY_KIND = { tokens: 'token', seats: 'seat', outcomes: 'outcome', storage: 'mb_storage' }; const setKindPrice = async (kind, price) => { const policy = policyRef.current; if (!policy) return; const credit = CREDIT_BY_KIND[kind]; if (!credit) return; await policy.doc.call('root.set_credit_price', credit, price); }; const setKindOverhead = async (kind, overhead) => { const policy = policyRef.current; if (!policy) return; const credit = CREDIT_BY_KIND[kind]; if (!credit) return; await policy.doc.call('root.set_credit_overhead', credit, overhead); }; // Entitlement calls use `kind` directly (kind IS the entitlement // name), not the credit mapping above. const setKindLimitValue = async (kind, value) => { const policy = policyRef.current; if (!policy) return; await policy.doc.call('root.set_entitlement_value', kind, value); }; const setKindLimitMode = async (kind, mode) => { const policy = policyRef.current; if (!policy) return; await policy.doc.call('root.set_entitlement_mode', kind, mode); }; const setKindReset = async (kind, resetIncSeconds) => { const policy = policyRef.current; if (!policy) return; await policy.doc.call('root.set_entitlement_reset_inc', kind, resetIncSeconds); }; // Seeds for UsageControls' inputs — there's no getter for price/ // overhead/mode/reset_inc yet (only setters), so these have to be // copied from demoPolicy's own starting values by hand. If the // policy text above changes, this needs updating to match — nothing // detects drift between the two automatically. // // TWO REAL CAVEATS, not guesses papered over: // - seats and storage both have `resets: false` with NO reset_inc // in the policy at all — defaulted to 0 here. Editing the Reset // control for either won't have a visible effect unless `resets` // itself also gets turned on, which isn't a control this exposes. // - storage's actual limit is `5MiB` (a Stof binary-mebibyte // literal), not a plain number — defaulted to 5 assuming an // MB-ish display matching demoKinds' own "Storage (MB)" label, // but MiB and MB aren't the same unit (1 MiB ≈ 1.048576 MB) — // worth a real check, not treated as exact here. const usageControlDefaults = { tokens: { price: 0.000004, overhead: 0.000003, limitValue: 1000, mode: 'soft', resetInc: 20 }, seats: { price: 5, overhead: 0, limitValue: 10, mode: 'hard', resetInc: 60 }, outcomes: { price: 0.25, overhead: 0.10, limitValue: 20, mode: 'soft', resetInc: 10 }, storage: { price: 0.003, overhead: 0.001, limitValue: 5, mode: 'soft', resetInc: 60 }, }; const MAX_POINTS = 120; const [datasets, setDatasets] = useState({ tokens: [], seats: [], outcomes: [], storage: [], denials: [], chargedUSD: [], // this is the "revenue over time" series overheadUSD: [], marginPct: [], }); const push = (arr, v, t) => (v == null ? arr : [...arr.slice(-(MAX_POINTS - 1)), { t, v }]); useEffect(() => { let cancelled = false; const tick = async () => { const [tokens, seats, outcomes, storage, denials, chargedUSD, overheadUSD] = await Promise.all([ getCurrentTokens(), getCurrentSeats(), getCurrentOutcomes(), getCurrentStorage(), getCurrentDenials(), getCurrentCustomerChargedUSD(), getCurrentOverheadUSD(), ]); if (cancelled) return; const t = Date.now(); const marginPct = chargedUSD != null && overheadUSD != null ? margin(chargedUSD, overheadUSD) : null; setDatasets((prev) => ({ tokens: push(prev.tokens, tokens, t), seats: push(prev.seats, seats, t), outcomes: push(prev.outcomes, outcomes, t), storage: push(prev.storage, storage, t), denials: push(prev.denials, denials, t), chargedUSD: push(prev.chargedUSD, chargedUSD, t), overheadUSD: push(prev.overheadUSD, overheadUSD, t), marginPct: push(prev.marginPct, marginPct, t), })); }; ensureStofReady() .then(() => runStof('', 'stof', { policyDoc: demoPolicy, policyDocFormat: 'stof', temp: false })) .then((result) => { if (cancelled) return; if (result.error || !result.policy) { // Surfaced as a console error rather than a thrown // exception — a broken demo policy shouldn't take the // whole docs page down, just fail to show the field. console.error('MonetizeFieldDemo: policy setup failed —', result.output); return; } policyRef.current = result.policy; result.policy.addHandler('update_datasets_handler', (key, value) => { const events = ['meter-overage', 'meter-reset', 'meter-changed', 'meter-limit']; if (events.includes(key)) { tick(); } }); setReady(true); }); return () => { cancelled = true; }; }, []); if (!ready) { return Loading the runtime…; } return ( <> ); } --- ## How it works Uses the `@formata/limitr` npm package with React and Chart.js for UI & graphing. The policy is kept simple — we define 4 credits for our usage particles, and 1 credit for keeping track of denials, just for the charting you see above (wouldn't add this in real life). - **Revenue Graph** — All overage charged to the customer over time (revenue captured). - **Overhead Graph** — Entire cost-to-deliver over time (all vendor/overhead costs). - **Margin Graph** — The portion of revenue kept over time. - **Usage per credit** — Meter value graphed for each entitlement-credit pair. - **Denials** — The total number of denials (`allow(...) -> false`) over time. :::tip [Spend caps](../concepts#spend-cap) in observe mode are used to capture all revenue and overhead metrics for charting. They are set to observe only and reset every minute. Both are used together to determine live margin. These are never simple `usage x price` calculations, since prices, overheads, limits, etc. are changing all the time. They do in real life also per customer, per override, per contract dates, etc. ::: ### Demo Setup 1. Create the Limitr policy: `await Limitr.new(policyYaml, 'yaml')`. 2. Create a customer on the `demo` plan. 3. Add revenue observation spend cap (in `usd`) to the demo customer. 4. Add overhead observation spend cap (in `usd`) to the demo customer. 5. Every particle carries a kind and quantity, calls `policy.allow(...)` every time it hits the decision line, continues on if allow returns true. 6. Add event handler (listens to `meter-limit`, `meter-changed`, `meter-overage`, `meter-reset`) that adds current values, revenue, overhead, and margin to a dataset. 7. Graph that dataset — redraws every time an event is seen and the dataset changes 8. Add simple UI controls for changing policy prices, etc. 9. You now have the demo you see above... For the particle spawn rates and quantity ranges, each particle is one of 4 kinds. Higher weights get spawned more frequently, and the amount will fall somewhere within the associated range (inclusive). ```typescript const demoKinds = [ { kind: 'tokens', label: 'AI Tokens', color: '#5b8dee', weight: 3, amountRange: [500, 4000] }, { kind: 'seats', label: 'Seats', color: '#9b7ede', weight: 1, amountRange: [-1, 3] }, { kind: 'outcomes', label: 'Outcomes', color: '#4dbbb0', weight: 2, amountRange: [1, 5] }, { kind: 'storage', label: 'Storage (MB)', color: '#c9a227', weight: 2, amountRange: [1, 10] }, ]; ``` ### Policy A single plan with the following (starting) entitlements for usage control & monetization: - **tokens** — charging $4/MTok, overhead of $3/MTok, 1000 included with plan every 20s, overage allowed & billed. - **seats** — charging $5/seat for overage, no overhead cost, hard limit of 10 seats (no overage allowed, no charges), resets every 60s for better graphs. - **outcomes** — charging $0.25/outcome, overhead of $0.1/outcome, 20 included with plan every 10s, overage allowed & billed. - **storage (MB)** — charging $0.003/MB, overhead of $0.001/MB, 5 MiB included with plan, overage allowed & billed, resets every 60s for better graphs. ```yaml policy: credits: token: price: { amount: 0.000004 } overhead_cost: 0.000003 stof_units: int seat: price: { amount: 5 } overhead_cost: 0 stof_units: int outcome: price: { amount: 0.25 } overhead_cost: 0.10 stof_units: int mb_storage: price: { amount: 0.003 } overhead_cost: 0.001 stof_units: MB denial: # for denial graph over time stof_units: int plans: demo: label: Demo Plan entitlements: tokens: limit: credit: token mode: soft value: 1000 # included with plan (not billed) minimum: 0 resets: true reset_inc: 20s seats: limit: credit: seat mode: hard value: 10 minimum: 0 resets: true reset_inc: 60s outcomes: limit: credit: outcome mode: soft value: 20 minimum: 0 resets: true reset_inc: 10s storage: limit: credit: mb_storage mode: soft value: 5MiB minimum: 0 resets: true reset_inc: 60s denials: limit: credit: denial mode: observe resets: true reset_inc: 1min ``` --- ## Credit Exchange Credit exchange table for unit and currency conversions. The exchange table is what allows a pool of abstract credits to drain across multiple discrete entitlements, and what enables margin calculation across any combination of resources. :::note This page is a monetization dive into the [Credits](../concepts#credit) and [Exchange](../concepts#exchange) concepts. ::: Each entry is an object that maps one credit by name to another via a multiplication factor: ```typescript exchange: { // credit_a_value = 0.5 * credit_b_value credit_a: { value: 0.5, currency: 'credit_b' } } ``` :::important Every credit that defines a price is automatically added to the exchange table, and is convertable based on that price. Remember we have a common base unit, the `rune`, for all prices, which allows them to be convertable. ::: ## Live Example Go ahead and expand this example policy by clicking on it, and it will reveal the following setup: - 2 credits defined: - Abstract `x_credit` — user-facing credit that can be used across all features & vendors - Claude Sonnet 5 AI credit to be used anywhere we call into Claude Sonnet 5 (simplified) - 3 explicit exchange table entries: - USD = 1 Rune = 1 USD (this is actually defined by default, but shown here for clarity) - x_credit = 0.1 * USD (so 1 credit = $0.10, or $1 = 10 credits) - euro = 1.14 * USD (1 Euro = 1.14 USD) - 1 plan (pro plan - for demo purposes only) - 1 entitlement (`ai_chat` -> Claude Sonnet 5 AI credits) - 10k Claude Sonnet 5 tokens included with plan - allows & charges for overage (soft limit) — anything beyond included + granted - 1 included topup - users on the pro plan get 50 `x_credit` included every month ### Run it export const example = { title: 'exchange', temp: true, showPolicy: true, policyFormat: 'stof', policyCollapsed: true, policy: ` policy: { credits: { x_credit: { description: "User-facing credit that can be used across the entire product" } claude_sonnet_5: { overhead_cost: 2e-6, price: { amount: 3e-6 } } } exchange: { // Included definition by default (in addition to the 'rune' itself) usd: { value: 1, currency: 'rune' } // Technically equivalent to setting a price on x_credit of $0.10, but for exchange purposes only x_credit: { value: 0.1, currency: 'usd' } // 1 Euro = 1.14 USD euro: { value: 1.14, currency: 'usd' } } plans: { pro: { label: 'Pro Plan' entitlements: { ai_chat: { description: 'AI chat feature' limit: { credit: 'claude_sonnet_5', mode: 'soft', value: 10_000, resets: true, reset_sch: 'monthly:1' } } } topups: { included: { description: "Pro users get 50 x_credits per month" credit: 'x_credit' value: 50 resets: true included: true reset_sch: 'monthly:1' } } } } }`, stof: ` // policy: { ... } #[main] fn exchange_example() { self.policy.create_customer('demo', 'pro'); self.policy.ensure_included_topups('demo'); // adds included grant of 50 x_credit // plan includes 10k ai_chat tokens before grants & overage, lets use 9k now assert(self.policy.allow('demo', 'ai_chat', 9_000)); assert_eq(self.policy.remaining('demo', 'ai_chat', grants=false), 1_000); pln('Used 9k of the 10k included AI tokens - 1k left\\n'); // remaining takes credit grants into account, using the exchange table! const rem = self.policy.remaining('demo', 'ai_chat').round(2); pln(\`\${rem} AI tokens left before overage starts (includes grant of x_credit)\`); pln(\`Which is 50 x_credits -> usd -> runes -> claude_sonnet_5 + 1k: \${Num.round(50 * 0.1 / 3e-6 + 1_000, 2)}\\n\`); // but how much is remaining in usd & euros? const ent = self.policy.entitlement('demo', 'ai_chat'); const usd = self.policy.credit_exchange(ent.limit.credit, 'usd', rem).round(3); // claude_sonnet_5 -> usd const euros = self.policy.credit_exchange(ent.limit.credit, 'euro', rem).round(2); // claude_sonnet_5 -> euro pln(\`$\${usd} (€\${euros}) of AI spend overhead left before any usage revenue captured\\n\`); // percentages are built in, also const perc_inc = self.policy.value('demo', 'ai_chat', percent=true, grants=false); const perc_gnt = self.policy.value('demo', 'ai_chat', percent=true, grants=true).round(3); pln(\`Used \${perc_gnt}% of total available right now, and \${perc_inc}% of included with plan\`); } `, typescript: ` const policy = await Limitr.new(\`policy: { ... }\`); await policy.createCustomer('demo', 'pro'); await policy.ensureCustomerIncludedTopups('demo'); // adds included grant of 50 x_credit // plan includes 10k ai_chat tokens before grants & overage, lets use 9k now await policy.allow('demo', 'ai_chat', 9000); console.log('Used 9k of the 10k included AI tokens - 1k left\\n'); // remaining takes credit grants into account, using the exchange table! const rem = await policy.remaining('demo', 'ai_chat'); console.log(\`\${Math.round(rem! * 100)/100} AI tokens left before overage starts (includes grant of x_credit)\`); console.log(\`Which is 50 x_credits -> usd -> runes -> claude_sonnet_5 + 1k: \${Math.round(50 * 0.1 / 3e-6 + 1_000 * 100) / 100}\\n\`); // but how much is remaining in usd & euros? const ent: any = await policy.entitlement('demo', 'ai_chat'); const usd = await policy.creditExchange(ent.limit.credit, 'usd', rem!); // claude_sonnet_5 -> usd const euros = await policy.creditExchange(ent.limit.credit, 'euro', rem!); // claude_sonnet_5 -> euro console.log(\`$\${Math.round(usd!*1000)/1000} (€\${Math.round(euros!*100)/100}) of AI spend overhead left before any usage revenue captured\\n\`); // percentages are built in, also const perc_inc = await policy.value('demo', 'ai_chat', true, false); const perc_gnt = await policy.value('demo', 'ai_chat', true, true); console.log(\`Used \${Math.round(perc_gnt!*1000)/1000}% of total available right now, and \${perc_inc}% of included with plan\`); `, }; ### What happened? 1. Created a demo customer & granted them their included topup of 50 `x_credit` 2. Used 9k of the 10k included Claude Sonnet 5 AI tokens with Pro Plan - Note: limit value is 10k, set to 0 if you want nothing included (grants & overage only) 3. Grabbed and printed the remaining ai_chat tokens using exchanges (in the defined credit units, `claude_sonnet_5`) 4. Converted the remaining ai_chat tokens to USD & Euros using the exchange table to see our remaining overhead before overage kicks in 5. Put the current AI chat meter value for this customer in terms of 2 percentages - Percent used of the total available to them right now (including their credit grants) - Percent used of the included tokens in their plan (10k limit, 9k used = 90%) --- ## Currency conversions For exchange purposes, currencies are modeled as credits. This is another reason to keep the [Rune](../concepts#the-rune) defined as **$1 USD** (default definition). The policy, unless otherwise overwritten, come with Rune `rune` and USD `usd` pre-defined, ready to use with spend caps, etc. :::info Limitr Cloud is completely separate and isolated for ledger and billing purposes. Additional flexibility exists on the managed side, including defining a policy exchange table, however, any rates defined or manipulated within your local policy are for **client use only**. ::: ### Adding explicit currency conversions Like the live example above, adding additional currencies can be done via the exchange table: :::note Remember, because `rune` and `usd` are pre defined as $1, you do not need to redefine them in your table. ::: ```typescript exchange: { // 1 Euro = 1.14 USD euro: { value: 1.14, currency: 'usd' } cfa_franc: { value: 0.0017, currency: 'usd' } } ``` What's needed to convert any currency to any other is a path in the exchange table to `rune`, which can be as many or as few steps as you need. Adding additional steps can be beneficial for modifying blocks of prices at once, like a `premium_ai_models` for example, that might add an additional cost multiplier to select models. ### Adding real-time currency lookups Limitr is a runtime, and runtimes do things like look up currency exchange rates when you need them to. :::warning This involves some Stof know-how, but should be straight-forward even without prior Stof knowledge. ::: #### 1. Add a Host function for currency lookups ```typescript // Load and init Limitr like normal (Limitr.cloud for Limitr Cloud) const policy = await Limitr.new(`policy: { /* your policy goes here */ }`); // Make this an HTTP fetch or whatever you need, return a JSON string with Exchange Pair async function currencyToExchangePairJSON(currency: string): Promise { console.log('looking up currency:', currency); // just adding for visibility/testing switch (currency) { case ('cfa_franc'): { return `{ "value": 0.0017, "currency": "usd" }`; } case ('euro'): { return `{ "value": 1.14, "currency": "usd" }`; } default: { return `{ "value": 1, "currency": "rune" }`; } } } // Expose the function to the Stof WASM sandbox so that we can use it in our policy runtime policy.doc.lib('Host', 'currency_exchange_lookup', currencyToExchangePairJSON); ``` #### 2. Replace the Exchange get_pair Stof function This is the part that involves some [Stof](https://stof.dev). We are going to replace the [Exchange prototype's](https://github.com/dev-formata-io/limitr/blob/main/src/spec/exchange.stof) `get_pair` function so that we can use our (TypeScript) Host to lookup exchange rates dynamically, and cache them in the policy for one hour, when it is then re-fetched lazily. ```typescript // Could parse right into the policy.exchange, but better in the prototype obj, always under root.LimitrTypes // Caches looked-up currency exchange rates for 1 hour each const lookupStof = ` fn get_pair(c: str!) -> ExchangePair { const cutoff = Time.now() - 1hr; let def = self.get(c); if (def != null && def.external && def.created < cutoff) { self.remove(c, shallow=false); def = null; } if (def == null) { const json = await ?Host.currency_exchange_lookup(c); if (json != null) { def = new { external: true, created: Time.now() }; parse(json, def, 'json'); self.insert(c, def); } } def }`; policy.doc.parse(lookupStof, 'stof', policy.doc.get('root.LimitrTypes.Exchange') as string); ``` #### 3. Try it out! Now, anytime we need an exchange rate that isn't explicitely defined, it is looked it up dynamically, marked as external, and cached in our policy for 1 hour before looking it up again to capture any changes. ```typescript // Now try it out... console.log(await policy.creditExchange('usd', 'cfa_franc', 1)); console.log(await policy.creditExchange('usd', 'cfa_franc', 1)); // cached, no additional log console.log(await policy.creditExchange('cfa_franc', 'euro', 4258)); ``` ```bash > bun run example.ts looking up currency: cfa_franc 588.2352941176471 588.2352941176471 looking up currency: euro 6.349649122807018 ``` --- ## USD spend caps One of the most common use-cases for the exchange table is placing a cap on spend using common units (USD) across many usage steps that are not directly using those units (credits, tokens, seconds, etc.). [Spend caps](../concepts#spend-cap) are very capable and span many use-cases, however, the most common is to either observe or control USD spend. Here's an example re-utilizing the policy defined from above: export const capExample = { title: 'spend-cap', temp: true, showPolicy: true, policyFormat: 'stof', policyCollapsed: true, policy: ` policy: { credits: { x_credit: { description: "User-facing credit that can be used across the entire product" } claude_sonnet_5: { overhead_cost: 2e-6, price: { amount: 3e-6 } } } exchange: { // Included definition by default (in addition to the 'rune' itself) usd: { value: 1, currency: 'rune' } // Technically equivalent to setting a price on x_credit of $0.10, but for exchange purposes only x_credit: { value: 0.1, currency: 'usd' } // 1 Euro = 1.14 USD euro: { value: 1.14, currency: 'usd' } } plans: { pro: { label: 'Pro Plan' entitlements: { ai_chat: { description: 'AI chat feature' limit: { credit: 'claude_sonnet_5', mode: 'soft', value: 10_000, resets: true, reset_sch: 'monthly:1' } } } topups: { included: { description: "Pro users get 50 x_credits per month" credit: 'x_credit' value: 50 resets: true included: true reset_sch: 'monthly:1' } } } } }`, stof: ` // policy: { ... } #[main] fn exchange_example() { self.policy.create_customer('demo', 'pro'); self.policy.ensure_included_topups('demo'); // adds included grant of 50 x_credit // Add a USD spend cap of $5 // Ignore grants is false so that our 50 x_credits count towards this cap (typically you'd want true here) // Overage only is false (the default) so that ALL usage is counted (typically you'd want true here) const cap = self.policy.add_customer_cap('demo', 5, 'usd_pipeline_cap', credit='usd', ignore_grants=false, overage_only=false ); // Now we spend - $5 / 3e-6 = 1_666_666.67 tokens assert(self.policy.allow('demo', 'ai_chat', 1_500_000)); pln(\`Spent $\${cap.meter_value} on 1.5 MTok\`); // Any spend over our $5 cap will be blocked, even with soft limits assert_not(self.policy.allow('demo', 'ai_chat', 1_000_000)); assert_eq(cap.meter_value, 4.5); pln(\`Used \${self.policy.value('demo', 'ai_chat')} tokens in pipeline\`); } `, typescript: ` await policy.createCustomer('demo', 'pro'); await policy.ensureCustomerIncludedTopups('demo'); // Add a USD spend cap of $5 await policy.addCustomerCap('demo', 5, { cap_id: 'usd_pipeline_cap', credit: 'usd', ignore_grants: false, // so our 50 x_credits count towards this cap (typically you'd want true here) overage_only: false, // so ALL usage is counted (typically you'd want true here) }); // Now we spend - $5 / 3e-6 = 1_666_666.67 tokens await policy.allow('demo', 'ai_chat', 1500000); let cap = await policy.customerCap('demo', 'usd_pipeline_cap'); console.log(\`Spent $\${cap?.meter_value} on 1.5 MTok\`); // Any spend over our $5 cap will be blocked, even with soft limits if (await policy.allow('demo', 'ai_chat', 1000000)) { throw Error('should not get here'); } const val = await policy.value('demo', 'ai_chat'); console.log(\`Used \${val} tokens in pipeline\`); `, }; --- ## Hybrid Pricing How to create hybrid pricing models. Every pricing model maps to usage. Seat-based pricing is the usage of seats. Flat, monthly pricing is just a quantity of one with recurring charges. Even a t-shirt purchase is just a quantity of one on your receipt, it just resets so that you aren't charged again. Therefore, there is only one type of pricing model in Limitr — hybrid. :::note[Limitr Cloud Users] Policy creation is made much simpler using our no-code UI. Any changes to strategy can be made right within the editor, automatically versioned, auditable, and publishable at the click of a button. Behind the scenes (and if you click developer view), you'll see a policy config similar to the ones below. Instead of adding JSON or Stof, you'll define credits and entitlements within the UI. However, behaviorally, they are identical to the ones defined on this page, and the integration is the same. ::: --- ## Seats Seats are straightforward — define a `seat` credit. export const seatExample = { title: 'seat-based', temp: true, showPolicy: true, policyFormat: 'stof', policyCollapsed: false, policy: ` policy: { credits: { // added a price for any future plans with soft limits that charge for seats seat: { description: 'A single seat', price: { amount: 19.99 } } } plans: { starter: { label: 'Starter Plan' entitlements: { seats: { description: 'Included seats with the plan' // mode is 'hard' by default, seats don't reset limit: { credit: 'seat', value: 10 } } } } } }`, stof: ` #[main] fn seats_example() { self.policy.create_customer('demo', 'starter'); const starting = self.policy.remaining('demo', 'seats'); pln(\`Starting with \${starting} seats\`); self.policy.increment('demo', 'seats'); self.policy.allow('demo', 'seats', 8); pln(\`Used \${self.policy.value('demo', 'seats')} seats, \${self.policy.remaining('demo', 'seats')} remaining\`); if (!self.policy.allow('demo', 'seats', 2)) { pln('Blocked!! Cannot use 2 more seats, limit is:', self.policy.limit('demo', 'seats')); } self.policy.decrement('demo', 'seats'); if (self.policy.allow('demo', 'seats', 2)) { pln('Success!! Used 2 more seats after decrement,', self.policy.value('demo', 'seats'), 'total'); } } `, typescript: ` const policy = await Limitr.new(\`policy: { ... }\`); // const policy = await Limitr.cloud({ token }); // for Limitr Cloud users // create a new customer if one doesn't already exist (locally or remotely via Cloud) await policy.ensureCustomer('demo', 'starter'); const starting = await policy.remaining('demo', 'seats'); console.log(\`Starting with \${starting} seats\`); await policy.increment('demo', 'seats'); await policy.allow('demo', 'seats', 8); console.log(\`Used \${await policy.value('demo', 'seats')} seats, \${await policy.remaining('demo', 'seats')} remaining\`); if (!await policy.allow('demo', 'seats', 2)) { console.log('Blocked!! Cannot use 2 more seats, limit is:', await policy.limit('demo', 'seats')); } await policy.decrement('demo', 'seats'); if (await policy.allow('demo', 'seats', 2)) { console.log('Success!! Used 2 more seats after decrement,', await policy.value('demo', 'seats'), 'total'); } `, }; If you need multiple prices, one per plan for example, define multiple credits with different prices: `starter_seat`, `pro_seat`. The `seats` entitlement would be the same name on every plan, and is the only name referenced from within your code. --- ## Customer Overrides How to override customer limits and prices. --- ## Overview Use Limitr to monetize usage. Monetizing usage means defining [credits](../concepts#credit) with a price, and [entitlements](../concepts#entitlement) that define control behavior (limits) for customers that reference credits (and prices). What gets charged is a single rule: overage can be billed, anything under the defined limit is not (it's included with the plan). Limits have one of 3 modes: - `hard` — never charges, overage not allowed as usage can never go above the limit - `soft` — allows overage, charges for the usage obove the limit at a price defined by the credit used - `observe` — never charges, overage cannot happen, analytics/metering only :::tip An entitlement limit can be thought of as the included plan usage, and it is valid to set it to zero — counting **all** usage as overage and billing accordingly if desired. ::: :::note The [Credit](../concepts#credit) is the only concept in Limitr that relates to money — any unit of value with a price, cost, or unit that matters to you or your customer (real or "fake") is a credit. ::: --- ## Limitr Cloud Integration Limitr Cloud is a fully managed and hosted platform for atomic usage ledgers per customer, invoicing, per-customer rates & limits, revenue analytics, payments/billing integrations — a fully built out platform for usage monetization. These docs define local policy definition examples (open-source & offline), which are not by themselves enough for usage monetization. With Limitr Cloud, your hosted policy is pulled down separately server-side for secure and accurate billing. The client-side local engine is responsible for usage events only — all prices, overhead, etc. is display-only. Client integration for Cloud, however, is the same as these docs apart from using `Limitr.cloud({token: apiToken})` instead of `Limitr.new('policy: {}')`. :::note Customer meters are independent of invoice/ledger quantities — meter resets are not required to align with billing periods. Some credits like AI tokens are typically reset in the ledger per billing period, where credits like seats are not. This is obvious in the Cloud UI, however, keep this in mind for the following examples, as any resets defined outline **meter** resets, not ledger resets (ledgers do not exist in the open-source engine). ::: :::warning[Implementation support] Our team is always here to support your needs, and usage control/monetization often has subtle differences product to product. The following examples give a high-level introduction to common patterns, but please reach out and we'll make sure you're setup for long-term success. ::: --- ## Basic seats ### 3 included with plan, no charges A `starter` plan that entitles a customer to a maximum of 3 seats, and does not allow them to go over. ```yaml policy: credits: seat: description: A single seat plans: starter: entitlements: seats: limit: credit: seat value: 3 mode: hard ``` ### 1 included with plan, $10/seat for every additional A `starter` plan that includes 1 seat, and charges $10 per additional seat. ```yaml policy: credits: seat: description: A single seat price: { amount: 10 } plans: starter: entitlements: seats: limit: credit: seat value: 1 mode: soft ``` ### $39.99 for every seat A `starter` plan that entitles customers to seats, charging for every one of them. ```yaml policy: credits: seat: description: A single seat price: { amount: 39.99 } plans: starter: entitlements: seats: limit: credit: seat value: 0 # every increment is overage mode: soft ``` --- ## Monthly plan subscription A soft limit that immediately charges overage, incremented once per customer and never reset. :::tip Any one-time purchase can be modeled this way — increment to purchase. Purchases like subscriptions repeat, where others, like an onboarding fee, don't. You can specify this right within the Limitr Cloud UI. ::: :::note Topup purchases have a dedicated path, they are independent from entitlements and limits. ::: ```yaml policy: credits: starter_subscription: price: { amount: 599.99 } pro_subscription: price: { amount: 899.99 } plans: starter: entitlements: subscription: limit: credit: starter_subscription value: 0 mode: soft pro: entitlements: subscription: limit: credit: pro_subscription value: 0 mode: soft ``` Each plan has a `subscription` entitlement. In code, you'd increment this with `policy.increment(userId, 'subscription')`, regardless of plan. :::tip In the SDK, there is a shortcut for adding a plan subscription quantity: `ensureCustomerPlanQuantity`. By default, this will increment a `subscription` entitlement if the meter value is less than 1. The subscription entitlement name can be set to the name of your choice by defining a string `subscription` field on each plan object. ::: --- ## AI Tokens For all discrete usage (tied to the real world), all entitlements should use credits defined as specificly as possible, so that usage can be attributed correctly to that vendor. To translate raw vendor usage into abstract outcomes, like `run_success` or anything like that, the pipeline should have all steps defined as discrete, vendor specific credits. There should then be an abstract `run_successes` entitlement and credit that gets incremented seperately at the end, optionally including spend, run, or customer metadata for additional attribution. Invoice line items would then include all usage, correctly accounted for, in all pipeline scenarios. :::note[Remember] Multiple entitlements are often stacked or combined to create desired behaviors. ::: ### Claude Sonnet 5 Input & Output Defining separate input and output credits for repackaged Claude Sonnet 5 tokens. ```yaml policy: credits: claude_sonnet_5_input: description: Input token for Claude Sonnet 5 ($3/MTok overhead) price: { amount: 0.000006 } # 50% margin per token overhead_cost: 0.000003 claude_sonnet_5_output: description: Output token for Claude Sonnet 5 ($15/MTok overhead) price: { amount: 0.00003 } # 50% margin per token overhead_cost: 0.000015 plans: starter: entitlements: home_page_ai_chat_input_tokens: # feature/product specific entitlement, not credit specific limit: credit: claude_sonnet_5_input value: 0 mode: soft resets: true reset_sch: 'monthly:1' # meter resets on the 1st of every month (UTC, lazy resets) home_page_ai_chat_output_tokens: limit: credit: claude_sonnet_5_output value: 0 mode: soft resets: true reset_sch: 'monthly:1' ``` #### Monetization/Billing Only Usage If you want to bill for usage only, it would then look something like: ```typescript const [inputCount, outputCount] = await callLLM(prompt); // charge for specific vendor-counted input and output tokens (monetization purposes, not control purposes) await limitrPolicy.allow(userId, 'home_page_ai_chat_input_tokens', inputCount); await limitrPolicy.allow(userId, 'home_page_ai_chat_output_tokens', outputCount); ``` #### Control Usage If you want to gate usage, `allow(...)` always returns a boolean value that you can use to do so. ```typescript const inputCount = await freeLLMCount(prompt); if (await limitrPolicy.allow(userId, 'home_page_ai_chat_input_tokens', inputCount)) { const [_, outputCount] = await callLLM(prompt); await limitrPolicy.allow(userId, 'home_page_ai_chat_output_tokens', outputCount); } ``` :::tip[founder tip] For controlling or gating AI usage, I prefer to split the responsibility completely. 2+ entitlements dedicated to just the billing, then one or more completely separate entitlement(s) and credit(s) for control. Sounds like it would add complexity, but in real life it simplifies things, and plays much nicer with custom tooling & helpers. This enables extremely accurate billing tied to the vendor, and also the flexibility for me to gate and control tokens on my own terms, with my own preferred counting. I would charge for the vendor tokens, and not for the control tokens. Pattern is top of function/pipeline/step count and gate (return early) with control only entitlements, then call into LLM/vendor, then at the bottom `allow(...)` with entitlements responsible for billing the user. ```typescript // gate/control usage const controlCount = await countTokensMyWay(prompt); if (!await limitrPolicy.allow(userId, 'control_only_tokens', controlCount)) return; // check does not change meters/state, just checks if allow would be true off of the rough count // optional part of the control step for spend caps to deny when only the vendor tokens have a price & over the cap if (!await limitrPolicy.check(userId, 'home_page_ai_chat_input_tokens', controlCount)) return; const [accurateInputCount, accurateOutputCount] = await callLLM(prompt); await limitrPolicy.allow(userId, 'home_page_ai_chat_input_tokens', accurateInputCount); await limitrPolicy.allow(userId, 'home_page_ai_chat_output_tokens', accurateOutputCount); ``` ::: --- ## GPU Seconds The Limitr runtime has native types for time. ```yaml policy: credits: gpu_second: description: One second of GPU time price: { amount: 0.001 } overhead_cost: 0.00012 stof_units: s plans: starter: entitlements: gpu_time: limit: credit: gpu_second value: 2min # 2 minutes included before overage starts mode: soft ``` Unit conversions happen automatically: ```typescript // meter 34 milliseconds, bill for any overage await limitrPolicy.allow(userId, 'gpu_time', 34 + 'ms'); // SDK always returns values in units of the credit (seconds) // Fun fact, remaining(..) takes credit grants into account by default using the exchange table (if applicable)! // Means if the user has 50 SuperCoolAppCredits for example, we'd convert those to gpu_seconds and tell you how much you have left to use right now const remainingSeconds = await limitrPolicy.remaining(userId, 'gpu_time'); ``` --- ## Outcomes An outcome is an abstract, app/product specific credit. Depending on the product, it can be something very tried and true, like `api_call` or `tool_call`, or, it can be something more specific to the customer, like `call_resolved` or `meeting_booked`. In Limitr, they work the same as any other credit: ```yaml policy: credits: api_call: description: One API call price: { amount: 0.001 } call_resolved: description: An AI agent resolved a call price: { amount: 0.10 } plans: starter: entitlements: api_calls: limit: credit: api_call value: 0 # charge for all of them! mode: soft calls_resolved: limit: credit: call_resolved value: 10 # plan includes 10 call resolutions, bills for everything after mode: soft ``` Implementation typically looks something like: ```typescript // Gate/control on whether the outcome would be allowed with check(...) - no meters change if (!await limitrPolicy.check(userId, 'calls_resolved', 1)) return; const metadata = { /* * Optional outcome metadata - spend, margin, details, whatever. * Useful for agent, pipeline, etc. usage attribution & analytics across your product. */ }; { /* * Pipeline of usage to reach outcome, populates metadata. * May or may not have many vendor calls, or discrete allow(...) steps for control/billing. */ } // Meter the outcome, bill for any overage, attach the metadata to the event (recorded in Limitr Cloud) await limitrPolicy.allow(userId, 'calls_resolved', 1, metadata); ``` --- ## Any kind of usage Limitr can monetize and control **any** type of usage. If it can be counted, it can be limited, capped, billed, and otherwise controlled. The catch-all credit units are `int` or `float`, able to count anything. But Stof also has native types for things like: time (`s`, `ms`, `hr`, etc.), memory (`bytes`, `GiB`, `TB`, `MB`, etc.), mass (`kg`, `g`, `lb`, etc.), length (`m`, `miles`, `cm`, etc.), angles (`deg`, `rad`), temperature (`F`, `C`, `K`), and more that can also be used. --- ## Credits Units your pricing is built on. Every entitlement limit is denominated in a credit. Every meter tracks consumption in a credit's units. Every margin calculation runs through a credit's `overhead_cost` and `price`. --- ## Discrete vs abstract \{#discrete-vs-abstract} **Discrete credits** map to something real: an AI input token, a GPU second, a megabyte of storage. They carry `overhead_cost` (what it costs you, per unit) and `price` (what you charge, per unit). This is what makes per-customer margin tracking accurate. **Abstract credits** are conceptual units — "AI Credits", "messages", "reports". No cost attached directly. They map to discrete credits through the [exchange table](./exchange). Use them when you want a customer-facing unit that doesn't expose per-token or per-MB pricing, or when you want a single credit pool to drain across multiple discrete entitlements. Both types use the same schema. The distinction is whether you define `overhead_cost`, `price`, and `stof_units`. --- ## Schema \{#schema} ```yaml credits: : description: string # Optional. Human-readable description. label: string # Human-facing label. Default: 'Credit'. unit: string # Singular unit label. Default: 'credit'. overhead_cost: float # Cost per unit (in runes). Default: 0. pricing_model: string # 'flat' | 'tiered' | 'volume' | 'stairstep'. Default: 'flat'. price: # Required if pricing_model is 'flat'. Null otherwise. amount: float # Price per unit (in runes). tiers: # Required if pricing_model is tiered/volume/stairstep. - up_to: float # Upper bound (exclusive). Omit on the last tier for infinity. price: amount: float stof_units: string # Unit type for this credit. Default: 'float'. resets: bool # Whether this credit's meter can reset. Default: false. ``` --- ## Fields \{#fields} ### `description` \{#description} Optional. Appears in event payloads and customer state objects. Useful for debugging and support tooling. ### `label` \{#label} Human-facing label. Shown in the Cloud dashboard and available in the customer object for UI display. ### `unit` \{#unit} Singular label for one unit of this credit. Used in display contexts. ### `overhead_cost` \{#overhead-cost} What this credit costs you per unit, expressed in runes (1 rune = 1 USD by default). Must be ≥ 0. Used by [`customerMarginSnapshot()`](./margin#customermargin) to compute cost-to-serve. The more accurately this reflects your actual provider cost, the more accurate your margin data. ### `pricing_model` \{#pricing-model} Controls how overage is billed on `soft`-limit entitlements. Must be one of: | Model | Behavior | |---|---| | `flat` | A single price per unit applied uniformly. Requires `price.amount`. Default. | | `tiered` | Graduated pricing — each consumption band has its own per-unit rate, applied only to units within that band. Requires `tiers`. | | `volume` | Total consumption determines a single per-unit rate, applied retroactively to all units. Requires `tiers`. | | `stairstep` | Total consumption determines which band you're in; you pay that band's flat fee regardless of where within the band you land. Requires `tiers`. | ### `price` \{#price} Required when `pricing_model` is `'flat'`. Must be `null` for all other pricing models. ```yaml price: amount: 0.000004 # per unit, in runes ``` ### `tiers` \{#tiers} Required when `pricing_model` is `'tiered'`, `'volume'`, or `'stairstep'`. A list of price tier objects. The last tier must omit `up_to` to represent infinity. Tiers are validated and sorted at policy load time. ```yaml tiers: - up_to: 100000 price: { amount: 0.000006 } - up_to: 1000000 price: { amount: 0.000005 } - # last tier: no up_to = infinity price: { amount: 0.000004 } ``` ### `stof_units` \{#stof-units} The unit type for this credit. When set, Limitr automatically converts values passed to `allow()` or `increment()` into the credit's canonical units before metering. ```yaml stof_units: MB # allow('user', 'storage', '2GB') → meters 2000 stof_units: min # allow('user', 'gpu_min', '42seconds') → meters 0.7 stof_units: int # token counts — no unit conversion, integer stof_units: float # (default) no conversion performed ``` Valid units include SI storage (`B`, `KB`, `MB`, `GB`, `TB`, `KiB`, `MiB`, `GiB`, `TiB`), time (`ms`, `s`, `min`, `hr`, `day`), and others defined in the Stof runtime. :::note[Unit conversion] Entitlement limit values are not auto-converted. Set `stof_units` to the unit you want the limit expressed in, and pass values in compatible units when calling `allow()`. ::: ### `resets` \{#resets} Whether meters for this credit can reset on a schedule. When `true`, the reset schedule is defined on the entitlement limit (`limit.resets`, `limit.reset_inc`) — not on the credit itself. This flags the credit as reset-capable; the entitlement limit controls when. --- ## Examples \{#examples} ### Discrete: AI token credit with flat pricing ```yaml credits: sonnet_input: description: Claude Sonnet 4 input tokens overhead_cost: 0.000003 # $3.00 per 1M tokens (your cost) pricing_model: flat price: amount: 0.000004 # $4.00 per 1M tokens (customer price) → $1 margin per 1M stof_units: int resets: true ``` ### Discrete: storage with tiered pricing ```yaml credits: gb_storage: description: Object storage overhead_cost: 0.00002 pricing_model: tiered tiers: - up_to: 10 price: { amount: 0.023 } - up_to: 50 price: { amount: 0.022 } - price: { amount: 0.021 } stof_units: GB ``` ### Abstract: customer-facing credit pool ```yaml credits: ai_credit: description: AI Credits label: AI Credit unit: credit # No overhead_cost, no price, no stof_units # Maps to discrete credits via the exchange table ``` --- ## SDK \{#sdk} ### `policy.credit()` \{#credit} Returns the credit record for a given credit ID. ```typescript const credit = await policy.credit('sonnet_input'); // { description, label, overhead_cost, pricing_model, price, stof_units, resets } ``` ### `policy.creditFor()` \{#creditfor} Returns the credit record for the credit backing a specific entitlement. `id` can be a plan ID or a customer ID. ```typescript const credit = await policy.creditFor('user_123', 'chat_input'); ``` ### `policy.creditExchange()` \{#creditexchange} Converts a value from one credit to another using the exchange table. Returns `null` if no exchange path exists. ```typescript // How many sonnet input tokens does 10 ai_credits buy? const tokens = await policy.creditExchange('ai_credit', 'sonnet_input', 10); ``` ### `policy.remainingCredit()` \{#remainingcredit} Returns the total remaining balance of a specific credit across all of a customer's grants, after exchange conversion. Does not include entitlement meter state. ```typescript const remaining = await policy.remainingCredit('user_123', 'ai_credit'); ``` --- ## Customers Any entity that consumes entitlements. A customer is any entity that consumes entitlements — a user, an organization, a workspace, a seat. Customers carry their own meters, grants, and overrides. They are the runtime state that Limitr enforces against. --- ## Schema \{#schema} The `Customer` type is managed by the SDK. You don't write customers into your policy document directly — you create and modify them through the API. ```typescript { id: string, // Primary ID. Must be unique per policy. plan: string, // Plan ID this customer is on. type: string, // Customer type: 'user', 'org', etc. Default: 'user'. label: string, // Human-facing label. Default: 'User'. alt_ids: string[], // Alternate IDs (e.g. Stripe customer ID, API key). refs: string[], // References to other customer IDs (e.g. org ID for a user). meters: {}, // Per-entitlement consumption state. Managed automatically. overrides: {}, // Per-entitlement limit overrides. See Entitlements. grants: {}, // Credit grants. See Topups & Grants. metadata: {}, // Optional. Arbitrary key-value data. } ``` --- ## Creating customers \{#creating} ### `policy.createCustomer()` \{#createcustomer} Creates a new customer and adds them to the policy. All arguments after `id` are optional. ```typescript // Minimal await policy.createCustomer('user_abc'); // On a specific plan await policy.createCustomer('user_abc', 'growth'); // Org customer with type and label await policy.createCustomer('org_xyz', 'enterprise', 'org', 'Acme Corp'); // User with a ref to their org await policy.createCustomer('user_abc', 'growth', 'user', 'Alice', ['org_xyz']); ``` Fires a `customer-set` event. If a Cloud connection is active, the customer is registered with Cloud. ### `policy.ensureCustomer()` \{#ensurecustomer} Creates the customer only if they don't already exist. Returns `true` if a new customer was created, `false` if they already existed. Prefer this over `createCustomer()` in cases where the customer may or may not exist. ```typescript await policy.ensureCustomer('user_abc', 'starter'); ``` --- ## Reading customer state \{#reading} ### `policy.customer()` \{#customer} Returns the full customer object — plan, meters, grants, overrides, and metadata. Use this to drive usage meters, plan badges, and upgrade prompts in your UI. ```typescript const customer = await policy.customer('user_abc'); // { // id: 'user_abc', // plan: 'growth', // type: 'user', // meters: { // chat_input: { credit: 'sonnet_input', value: 245000, started: 1718000000000 } // }, // grants: { // 'lgrnt_abc123': { credit: 'ai_credit', value: 7.3, starting_value: 10, ... } // }, // overrides: {}, // metadata: null // } ``` ### `policy.customers()` \{#customers} Returns all customers as a single record. Useful for persistence — this is the complete customer state snapshot. ```typescript const all = await policy.customers(); ``` ### `policy.customerMetadata()` \{#customermetadata} Returns only the metadata object for a customer. ```typescript const meta = await policy.customerMetadata('user_abc'); ``` --- ## Modifying customers \{#modifying} ### `policy.setCustomerPlan()` \{#setcustomerplan} Changes a customer's plan. When `overwrite_meters` is `true` (default), resets all meters. Fires `customer-set` and `customer-plan-changed` events. ```typescript await policy.setCustomerPlan('user_abc', 'enterprise'); ``` ### `policy.setCustomer()` \{#setcustomer} Sets the entire customer object at once. The preferred way to apply multiple changes in a single operation. ```typescript await policy.setCustomer(customer); // emits events and updates Cloud ``` ### `policy.removeCustomer()` \{#removecustomer} Removes a customer from the local policy. Fires `customer-removed`. :::note[Cloud customers] `removeCustomer()` does not remove the customer from Limitr Cloud. Use the Cloud dashboard for Cloud customer management. ::: ```typescript await policy.removeCustomer('user_abc'); ``` --- ## Alternative IDs \{#alt-ids} Customers can carry multiple IDs — useful for mapping a Stripe customer ID, an API key, or any external identifier to the same Limitr customer. Alternative IDs must be unique per policy. ```typescript // Add an alt ID await policy.addAltID('user_abc', 'cus_stripe_xyz'); // Both resolve to the same customer await policy.customer('user_abc'); await policy.customer('cus_stripe_xyz'); // Remove an alt ID await policy.removeAltID('cus_stripe_xyz'); ``` All enforcement methods accept primary IDs or alt IDs interchangeably. ### `policy.addAltID()` \{#addaltid} Adds an alternative ID to an existing customer. When `event` is `true` (default), fires a `customer-set` event. ### `policy.removeAltID()` \{#removealtid} Removes an alternative ID. The primary ID and all other alt IDs remain valid. --- ## Customer references \{#refs} The `refs` field is a list of other customer IDs. References enable org-scoped entitlements: when a user references an org, entitlements with `scope: org` resolve against the org's meters rather than the user's. ```typescript // Create org await policy.createCustomer('org_xyz', 'enterprise', 'org', 'Acme Corp'); // Create user with a ref to the org await policy.createCustomer('user_abc', 'growth', 'user', 'Alice', ['org_xyz']); // Enforcement on a scope:org entitlement draws from org_xyz's meter await policy.increment('user_abc', 'seats'); // → org_xyz's seat pool ``` ### `policy.customerRefs()` \{#customerrefs} Returns the list of referenced customer IDs. ```typescript const refs = await policy.customerRefs('user_abc'); // ['org_xyz'] ``` --- ## Metadata \{#metadata} Arbitrary key-value data attached to a customer. Useful for storing external system IDs, feature flags, or any state you want accessible at enforcement time. :::tip[Capture email early] Metadata is important for customer segments, price modifications like coupons, and billing workflows in Limitr Cloud. At minimum, capture `email` if you plan to use the customer object for billing — it's much easier to add it at creation time than to backfill later. ::: ```typescript await policy.createCustomer('user_abc', 'starter', 'user', 'Alice', null, null, { stripe_id: 'cus_xyz', account: 'example', email: 'user@example.com', internal_tier: 'pilot', }); const meta = await policy.customerMetadata('user_abc'); // { stripe_id: 'cus_xyz', account: 'example', email: 'user@example.com', internal_tier: 'pilot' } ``` --- ## Subscription billing \{#subscription} `ensureCustomerPlanQuantity()` increments the plan's subscription entitlement if the customer's meter is below `1`. Call this once per billing period or at plan activation to trigger the subscription charge event. ```typescript await policy.ensureCustomerPlanQuantity('user_abc'); ``` Requires a `subscription` entitlement on the plan — typically a `soft`-limit entitlement with `value: 0` so the first increment fires a `meter-overage` event that your billing code handles. See [Plans → subscription](./plans#subscription) for setup. Does nothing until the trial period has elapsed, if `trial_period` is set on the plan. --- ## Enforcement Where application code meets policy. The enforcement API is where your application code meets the Limitr policy. Every operation runs in-process via WebAssembly — no network calls, sub-millisecond latency on the hot path. In Cloud mode, usage syncs asynchronously; enforcement decisions are never gated on network availability. --- ## Core operations ### `policy.allow()` \{#allow} The primary enforcement and metering method. If the operation is within the customer's entitlement, the meter is updated and `true` is returned. If blocked, `false` is returned and a `meter-limit` event fires. ```typescript allow( customer: string, entitlement: string, value: number | string = 0, // amount to consume; 0 = boolean check event: boolean = true // whether to fire events ): Promise ``` `value` can be a raw number or a unit string if the credit has `stof_units` defined: ```typescript await policy.allow('user_abc', 'chat_input', 4200); // 4200 tokens await policy.allow('user_abc', 'file_storage', '500MB'); // unit string await policy.allow('user_abc', 'chat_access'); // boolean check (value = 0) ``` When `value` is `0` and the entitlement has no limit, `allow()` returns `true`. This is the boolean access check pattern. --- ### `policy.increment()` \{#increment} Consumes the entitlement's defined `limit.increment` value (default: `1`). Returns `true` if within limit, `false` if blocked. Equivalent to `allow(id, entitlement, increment)`. ```typescript if (await policy.increment('org_xyz', 'seats')) { await db.addMember(userId, orgId); } else { return { error: 'Seat limit reached' }; } ``` --- ### `policy.decrement()` \{#decrement} Releases the entitlement's defined `limit.increment` value. Used for resources that can be returned — removing a seat, deleting a file, closing a workspace. Respects `limit.minimum` if defined. ```typescript await policy.decrement('org_xyz', 'seats'); ``` --- ### `policy.check()` \{#check} Read-only. Returns whether an `allow()` call with this value would succeed, without updating the meter. Use for pre-authorization checks — UI gates, pre-flight checks before expensive operations. ```typescript // Pre-flight check — don't consume yet const authorized = await policy.check('user_abc', 'chat_input', estimatedTokens); if (!authorized) return { error: 'Insufficient token balance' }; // Consume after the operation with actual usage await policy.allow('user_abc', 'chat_input', actualTokens); ``` --- ### `policy.set()` \{#set} Sets the meter to an absolute value by computing `allow(value - current)`. Use for storage or state-based metering where you know the total, not the delta. ```typescript await policy.set('user_abc', 'file_storage', currentUsageBytes); ``` --- ## Read operations ### `policy.value()` \{#value} Returns the current meter value for a customer's entitlement. ```typescript value( customer: string, entitlement: string, percent: boolean = false, // if true, returns % of limit (0–100) grants: boolean = true // include grant balances in percentage calculation ): Promise ``` ```typescript const used = await policy.value('user_abc', 'chat_input'); // raw count const usedPct = await policy.value('user_abc', 'chat_input', true); // % of limit ``` Returns `null` if the customer or entitlement doesn't exist. --- ### `policy.remaining()` \{#remaining} Returns the remaining balance (`limit - value`). Includes credit grant balances in the effective limit when `grants` is `true` (default). ```typescript const left = await policy.remaining('user_abc', 'chat_input'); const leftPct = await policy.remaining('user_abc', 'chat_input', true); // as % ``` --- ### `policy.allowance()` \{#allowance} Returns how much of an entitlement a customer can consume right now — the binding constraint of either the governor token balance or the remaining period balance, whichever is smaller. Falls back to `remaining()` when no governor is configured. Use this to pre-size operations before submitting them so they're guaranteed to pass `allow()`. ```typescript const budget = await policy.allowance('user_abc', 'ai_tokens'); // Size your operation to budget, then call allow() ``` --- ### `policy.projectedExhaustion()` \{#projectedexhaustion} Returns the estimated time in milliseconds until a customer's entitlement is exhausted at their current rate of consumption. Returns `null` if there is no consumption history or the rate is zero. Meaningful after at least two `allow()` calls. Pass `smoothed: true` to use the EWMA rate instead of the instantaneous rate — better for alerting since it doesn't overreact to bursts. ```typescript projectedExhaustion( customer: string, entitlement: string, smoothed: boolean = false, grants: boolean = true ): Promise // ms until exhaustion, or null ``` ```typescript const msLeft = await policy.projectedExhaustion('user_abc', 'ai_tokens', true); if (msLeft !== null && msLeft < 86_400_000) { // Customer will exhaust within 24 hours — trigger upsell flow } ``` --- ### `policy.rate()` \{#rate} Returns the current instantaneous consumption rate in units per millisecond, derived from the last two `allow()` calls. Returns `0` if fewer than two calls have been made. ```typescript const rate = await policy.rate('user_abc', 'ai_tokens'); // tokens/ms ``` --- ### `policy.acceleration()` \{#acceleration} Returns the rate of change between the two most recent consumption intervals. Positive means consumption is speeding up, negative means slowing down. Returns `0` if fewer than three `allow()` calls have been made. ```typescript const acc = await policy.acceleration('user_abc', 'ai_tokens'); if (acc > 0) { // Consumption is accelerating — check projected exhaustion } ``` --- ### `policy.resets()` \{#resets} Returns the timestamp (unix ms) when this entitlement's meter will next reset. Returns `null` if the entitlement doesn't reset. ```typescript const nextReset = await policy.resets('user_abc', 'ai_tokens'); ``` --- ### `policy.limit()` \{#limit} Returns the enforced limit value, including any active customer override. When `grants` is `true` (default), adds the customer's applicable grant balance to the effective limit. ```typescript const limit = await policy.limit('user_abc', 'chat_input'); ``` --- ### `policy.cost()` \{#cost} Returns the rune cost of a standard `increment()` on this entitlement. Useful for billing calculations. ```typescript const cost = await policy.cost('growth', 'seats'); // cost per seat add ``` --- ## Events Every enforcement operation can fire one of four events. Subscribe with [`addHandler()`](#addhandler). | Event | When it fires | |---|---| | `meter-changed` | Every successful `allow()`, `increment()`, or `decrement()` that updates a meter | | `meter-limit` | When `allow()` or `increment()` is blocked by a `hard` limit | | `meter-overage` | When `allow()` or `increment()` exceeds a `soft` limit and grants are exhausted | | `meter-governed` | When `allow()` is blocked by a governor rate ceiling before the hard limit is reached | ### Event payload shape \{#payload} All three metering event types share the same base structure. `meter-governed` includes additional governor context. The `value` argument passed to your handler is a JSON string. ```typescript { customer: { id: string, plan: string, type: string, }, entitlement: string, // entitlement name plan: string, // plan ID credit: { description: string, // credit description // ... other credit fields }, meter: { value: number, // meter value after operation limit: number, // effective limit invalid: number, // value that would have been set (limit events only) // ... other meter fields }, overage: number, // meter-overage only: amount over limit after grants grant_value_applied: number, // meter-overage only: how much grant covered governor: { // meter-governed only tokens: number, // token balance at time of denial capacity: number, // governor bucket capacity requested: number, // amount requested }, } ``` --- ## Event handler API ### `policy.addHandler()` \{#addhandler} Registers a named event handler. All events from all operations are routed through every registered handler. ```typescript policy.addHandler('my-handler', (key: string, value: unknown) => { const event = JSON.parse(value as string); if (key === 'meter-changed') { // fires on every successful consumption } if (key === 'meter-limit') { // fires when a hard limit blocks a request notifySupport(event.customer.id, event.entitlement); } if (key === 'meter-overage') { // fires when a soft limit is exceeded and grants are exhausted billing.queueCharge(event.customer.id, event.entitlement, event.overage); } }); ``` ### `policy.removeHandler()` \{#removehandler} Removes a handler by name. Returns `true` if it existed. ```typescript policy.removeHandler('my-handler'); ``` ### `policy.clearHandlers()` \{#clearhandlers} Removes all registered handlers. --- ## Patterns ### Feature gate ```typescript const canExport = await policy.check('user_abc', 'pdf_export'); if (!canExport) return { error: 'Upgrade to export PDFs' }; ``` ### AI token metering with access check ```typescript if (!await policy.check('user_abc', 'chat_access')) { return { error: 'No chat access on this plan' }; } if (await policy.allow('user_abc', 'chat_input', estimatedTokens)) { const response = await callLLM(prompt); // Meter actual output after the call await policy.allow('user_abc', 'chat_output', response.usage.output_tokens); } else { return { error: 'Token limit reached' }; } ``` ### Seat-based SaaS with org scope ```typescript if (await policy.increment('org_xyz', 'seats')) { await db.addMember(userId, orgId); } else { return { error: 'Seat limit reached. Upgrade or remove a member.' }; } // When a member is removed await policy.decrement('org_xyz', 'seats'); ``` ### Storage with unit conversion ```typescript if (await policy.allow('user_abc', 'file_storage', `${fileBytes}bytes`)) { await uploadFile(file); } else { return { error: 'Storage limit exceeded' }; } ``` ### Soft-limit overage billing ```typescript policy.addHandler('billing', (key, value) => { if (key === 'meter-overage') { const event = JSON.parse(value as string); // event.overage is what wasn't covered by grants billing.queueCharge(event.customer.id, event.credit, event.overage); } }); ``` ### Governor rate limiting ```typescript policy.addHandler('governor', (key, value) => { if (key === 'meter-governed') { const event = JSON.parse(value as string); // Customer hit the rate ceiling, not the hard limit // event.governor.tokens — bucket balance at denial // event.governor.requested — what they asked for return { error: 'Rate limit reached. Please slow down.', retryAfter: 2000 }; } }); ``` ### Projected exhaustion alerting ```typescript policy.addHandler('alerts', async (key, value) => { if (key === 'meter-changed') { const event = JSON.parse(value as string); const msLeft = await policy.projectedExhaustion( event.customer.id, event.entitlement, true // smoothed — doesn't overreact to bursts ); if (msLeft !== null && msLeft < 24 * 60 * 60 * 1000) { await notifyCustomerSuccess(event.customer.id, event.entitlement); } } }); ``` --- ## Entitlements Resources a customer on a plan is allowed to use. Entitlements live inside plan definitions, identified by a string ID. An entitlement without a limit is a boolean flag — present means the customer has access. An entitlement with a limit is metered. --- ## Entitlement schema \{#schema} ```yaml entitlements: : description: string # Optional. Appears in event payloads. hidden: bool # Exclude from public serialization. Default: false. scope: string # Optional. Customer type scope (e.g. 'org'). limit: # Optional. Omit for a boolean access entitlement. ``` ### `description` \{#description} Human-readable description. Included in event payloads (`meter-limit`, `meter-overage`, `meter-changed`) and in the customer object. Useful for support tooling and dashboards. ### `hidden` \{#hidden} If `true`, the entitlement is excluded from public policy serialization. Use for internal metering that customers shouldn't see. ### `scope` \{#scope} Ties the entitlement to a customer type. When set, enforcement operations for this entitlement look through the customer's `refs` to find a customer of the matching type, and operate against that customer's meter instead. The canonical use case is org-scoped entitlements — a `seats` entitlement with `scope: org` means all users who ref the same org draw from the org's shared seat pool, not their individual meters. ```yaml seats: scope: org limit: credit: seat mode: hard value: 10 increment: 1 ``` Any member of the org calling `policy.increment('user_123', 'seats')` draws from the org's pool. --- ## Limit schema \{#limit-schema} ```yaml limit: credit: string # Required. Credit ID for this limit. mode: string # 'hard' | 'soft' | 'observe'. Default: 'hard'. grants_apply: bool # Whether grants can extend this limit. Default: true. value: float | string # Maximum value. Supports unit strings (e.g. '2GiB'). Default: 0. increment: float | string # Amount per increment()/decrement(). Default: 1. minimum: float | string # Optional floor value for decrement(). resets: bool # Does this meter reset? Default: false. reset_inc: duration # Duration-based reset interval. Mutually exclusive with reset_sch. reset_sch: string # Calendar reset schedule. Mutually exclusive with reset_inc. override_expires_on: ms # Expiry for customer overrides only. governor_enabled: bool # Rate ceiling below the hard limit. Default: false. governor_capacity: float | string # Max burst tokens. Required when governor_enabled is true. governor_refill_rate: float # Tokens per ms refill rate. Required when governor_enabled is true. ewma_alpha: float # EWMA smoothing factor (0.0–1.0). Default: 0.2. ``` ### `credit` \{#credit} The credit ID this limit is denominated in. Must reference a credit defined in `policy.credits`. Required. ### `mode` \{#mode} Controls what happens when consumption reaches the limit value. | Mode | Behavior | |---|---| | `hard` | Blocks the operation. `allow()` returns `false`. Fires `meter-limit`. No overage unless covered by a grant. **Default.** | | `soft` | Allows the operation. Fires `meter-overage`. Limitr draws from applicable grants first before firing the event. | | `observe` | Never blocks. Meters indefinitely. No enforcement. Useful for tracking usage without restricting it. | ### `value` \{#value} The enforced maximum. When a customer's meter reaches this value, `hard` mode blocks and `soft` mode fires overage. Supports raw floats or unit strings when the credit has `stof_units` defined. ```yaml value: 500000 # raw float value: '2GiB' # unit string — converted to the credit's stof_units value: 0 # soft limit of 0: every increment fires meter-overage immediately ``` ### `increment` \{#increment} The amount consumed or released per `increment()` / `decrement()` call. Defaults to `1`. Supports unit strings. ```yaml increment: 1 # one seat per call increment: '100MB' # 100MB per upload slot ``` ### `minimum` \{#minimum} An optional floor value. `decrement()` will not reduce the meter below this value. ### `grants_apply` \{#grants-apply} Whether credit grants can extend this limit. Default `true`. Set to `false` on enforcement or rate-governing entitlements where grants should never punch through a rate ceiling, or on `observe`-mode entitlements where you want a clean, unmodified consumption signal. ```yaml # Enforcement entitlement — grants don't extend this ceiling ai_tokens_daily: limit: credit: ai_token mode: hard grants_apply: false value: 50000 resets: true reset_sch: 'monthly:1' ``` ### `resets`, `reset_inc`, and `reset_sch` \{#resets} Whether and how the meter resets. When `resets: true`, set either `reset_inc` for a fixed duration or `reset_sch` for a calendar schedule. The two are mutually exclusive. If neither is set, `reset_inc` defaults to `30days`. **Duration-based reset (`reset_inc`)** The meter resets on a fixed interval from when it was last reset. ```yaml resets: true reset_inc: 1day # resets every 24 hours reset_inc: 30days # resets every 30 days (default) ``` Valid time units: `ms`, `s`, `min`, `hr`, `day`, `days`. **Calendar-based reset (`reset_sch`)** The meter resets on a calendar boundary, regardless of when the customer was created. All schedules use UTC. ```yaml resets: true reset_sch: 'monthly:1' # 1st of every month reset_sch: 'monthly:15' # 15th of every month reset_sch: 'monthly:last' # last day of every month reset_sch: 'weekly:mon' # every Monday reset_sch: 'nth_weekday:1:tue' # first Tuesday of every month reset_sch: 'nth_weekday:2:fri' # second Friday of every month ``` --- ### `governor_enabled`, `governor_capacity`, `governor_refill_rate` \{#governor} An optional rate ceiling below the hard limit. When enabled, consumption is shaped before the wall is reached — customers are governed rather than cut off. The governor uses a token bucket model: the bucket holds up to `governor_capacity` tokens and refills at `governor_refill_rate` tokens per millisecond. Each `allow()` call consumes tokens equal to the requested value. When the bucket is empty, the request is denied even if the hard limit hasn't been reached, and a `meter-governed` event fires. ```yaml limit: credit: ai_token mode: hard value: 1000000 resets: true reset_sch: 'monthly:1' governor_enabled: true governor_capacity: 50000 # max burst: 50k tokens at once governor_refill_rate: 0.5 # sustained rate: 500 tokens/sec (0.5/ms) ``` The governor is most useful for enterprise customers with SLA obligations — it prevents a hard cutoff mid-operation while still protecting your infrastructure cost. Use `allowance()` to pre-size operations against the current governor state. `governor_capacity` and `governor_refill_rate` are required when `governor_enabled: true`. ### `ewma_alpha` \{#ewma-alpha} The smoothing factor for the exponentially weighted moving average rate calculation (0.0–1.0). Lower values smooth more aggressively (better for bursty workloads). Higher values track recent changes faster. Default: `0.2`. Used by `projectedExhaustion(smoothed: true)` for trend-based alerting rather than instantaneous rate projection. --- ## Boolean entitlements \{#boolean} An entitlement with no `limit` field is a boolean flag. `allow()` or `check()` against it returns `true` if the entitlement exists on the plan, `false` if it doesn't. ```yaml entitlements: pdf_export: description: Access to PDF export feature ``` ```typescript const canExport = await policy.check('user_123', 'pdf_export'); if (!canExport) return { error: 'Upgrade to export PDFs' }; ``` --- ## Customer overrides \{#overrides} An override replaces the limit for a specific customer without changing the plan. Any limit field can be overridden. Overrides can have an expiry, after which the customer reverts to their plan's limit. ```typescript // Give enterprise_org 500 seats instead of the plan default await policy.createCustomerOverride('enterprise_org', 'seats', 500); // Override with an expiry await policy.createCustomerOverride( 'user_123', 'chat_input', 2000000, Date.now() + 30 * 24 * 60 * 60 * 1000 // expires in 30 days ); // Remove an override — customer reverts to plan limit await policy.removeCustomerOverride('user_123', 'chat_input'); ``` Full signature: ```typescript policy.createCustomerOverride( id: string, // customer ID entitlement: string, // entitlement name value?: string | number, // limit value expires_on?: number, // expiry timestamp (ms) credit?: string, // credit ID (if changing the credit) mode?: string, // 'hard' | 'soft' | 'observe' increment?: number | string, resets?: boolean, reset_inc?: number | string, // mutually exclusive with reset_sch reset_sch?: string // mutually exclusive with reset_inc ): Promise // returns override node ID, or null on failure ``` --- ## SDK \{#sdk} ### `policy.entitlement()` \{#entitlement} Returns the entitlement record for a given plan ID or customer ID and entitlement name. Includes the resolved limit with any customer override applied. ```typescript const ent = await policy.entitlement('user_123', 'chat_input'); // { description, limit: { credit, mode, value, resets, reset_inc, reset_sch, ... } } ``` ### `policy.limit()` \{#limit} Returns the enforced limit value for a customer's entitlement. Includes credit grant balances in the effective limit when `grants` is `true` (default). Returns `null` if no limit is defined. ```typescript const limit = await policy.limit('user_123', 'chat_input'); // with grants const limit = await policy.limit('user_123', 'chat_input', false); // without grants ``` ### `policy.remaining()` \{#remaining} Returns the remaining balance (`limit - value`). Includes grant balances by default. ```typescript const left = await policy.remaining('user_123', 'chat_input'); ``` ### `policy.allowance()` \{#allowance} Returns how much of an entitlement a customer can consume right now — the binding constraint of either the governor token balance or the remaining period balance, whichever is smaller. Falls back to `remaining()` when no governor is configured. Use this to pre-size operations before submitting them, so they're guaranteed to pass `allow()`. ```typescript const budget = await policy.allowance('user_123', 'ai_tokens'); const duration = Math.min(desiredDuration, budget); if (await policy.check('user_123', 'ai_tokens', duration)) { await policy.allow('user_123', 'ai_tokens', duration); generateVideo(duration); } ``` ### `policy.projectedExhaustion()` \{#projectedexhaustion} Returns the estimated time in milliseconds until a customer's entitlement is exhausted at their current rate of consumption. Returns `null` if there is no consumption history or the rate is zero. Meaningful after at least two `allow()` calls. ```typescript // Instantaneous rate projection const msUntilEmpty = await policy.projectedExhaustion('user_123', 'ai_tokens'); // Smoothed rate projection (EWMA) — better for alerting const msSmoothed = await policy.projectedExhaustion('user_123', 'ai_tokens', true); if (msSmoothed !== null && msSmoothed < 24 * 60 * 60 * 1000) { // Customer will exhaust their allocation within 24 hours notifyCustomerSuccess(customerId); } ``` Parameters: `customer, entitlement, smoothed? (default false), grants? (default true)` ### `policy.rate()` \{#rate} Returns the current instantaneous consumption rate in units per millisecond, derived from the last two `allow()` calls. Returns `0` if fewer than two calls have been made. ```typescript const rate = await policy.rate('user_123', 'ai_tokens'); // tokens/ms ``` ### `policy.acceleration()` \{#acceleration} Returns the rate of change between the two most recent consumption intervals. Positive means consumption is speeding up, negative means slowing down. Returns `0` if fewer than three `allow()` calls have been made. ```typescript const acc = await policy.acceleration('user_123', 'ai_tokens'); ``` ### `policy.resets()` \{#resets-sdk} Returns the timestamp (unix ms) when this entitlement's meter will next reset. Returns `null` if the entitlement doesn't reset. ```typescript const nextReset = await policy.resets('user_123', 'ai_tokens'); // Display as a date: new Date(nextReset).toLocaleDateString() ``` ### `policy.createCustomerOverride()` \{#createcustomeroverride} See [Customer overrides](#overrides) above. ### `policy.removeCustomerOverride()` \{#removecustomeroverride} Removes a customer override. The customer reverts to their plan's limit. ```typescript await policy.removeCustomerOverride('user_123', 'chat_input'); ``` --- ## Patterns ### Layered governors with billing meter The recommended pattern for enterprise AI products: multiple hard-limited enforcement entitlements govern rate and period, with a soft-limited entitlement recording billable consumption. ```yaml entitlements: ai_tokens_daily: limit: credit: ai_token mode: hard grants_apply: false value: 50000 resets: true reset_inc: 1day governor_enabled: true governor_capacity: 5000 governor_refill_rate: 0.058 ai_tokens_monthly: limit: credit: ai_token mode: hard grants_apply: false value: 1000000 resets: true reset_sch: 'monthly:1' governor_enabled: true governor_capacity: 50000 governor_refill_rate: 0.5 ai_tokens_billing: limit: credit: ai_token mode: soft value: 0 ``` ```typescript const budget = Math.min( await policy.allowance(id, 'ai_tokens_daily'), await policy.allowance(id, 'ai_tokens_monthly') ); if ( await policy.check(id, 'ai_tokens_daily', budget) && await policy.check(id, 'ai_tokens_monthly', budget) ) { await policy.allow(id, 'ai_tokens_daily', budget); await policy.allow(id, 'ai_tokens_monthly', budget); await policy.allow(id, 'ai_tokens_billing', budget); // always records } ``` --- ## Exchange Map credits into one another. The exchange table maps credits to one another through a common base currency — the **rune**. It's what allows a pool of abstract credits to drain across multiple discrete entitlements, and what enables Limitr to compute per-customer margin without external tooling. --- ## The rune \{#rune} Every value in the exchange table is expressed relative to the rune. By default, 1 rune = 1 USD. You don't need to define the rune explicitly — it's automatically set as: ```yaml exchange: rune: value: 1 currency: usd ``` All other exchange pairs chain back to the rune. Limitr resolves any conversion by walking the chain: `credit A → credit B → rune`. --- ## Schema \{#schema} ```yaml exchange: grant_strategy: string # 'expires_first' | 'cheapest_first' | 'valuable_first'. Default: 'expires_first'. rune: value: float # Always 1 by convention. currency: string # 'usd' or another terminal currency. : value: float # How many units of `currency` does 1 unit of this credit equal? currency: string # The credit ID or terminal currency this pair prices into. ``` Each exchange pair has two fields: **`value`** — The multiplier. Must be ≥ 0. Represents how many units of `currency` one unit of this credit is worth. **`currency`** — The credit ID or terminal currency this pair prices into. Use `'rune'` to price directly in runes. Use another credit's ID to chain through it. :::tip[Matching price to exchange] Setting a credit's exchange `value` equal to its `price.amount` keeps the two in sync and makes the policy easier to maintain. Chains let you adjust the relationship between abstract and discrete credits independently of the base price. ::: --- ## How resolution works \{#resolution} Limitr resolves conversions by walking the chain from the source credit toward `rune`, multiplying values at each step. If a cycle is detected, the conversion returns `null`. For credits with a `flat` pricing model and a defined `price.amount`, an explicit exchange entry is optional — Limitr uses the credit's price as its rune value automatically. Adding an explicit entry overrides this. **Example:** ```yaml exchange: rune: { value: 1, currency: usd } ai_credit: { value: 1.25, currency: rune } sonnet_input: { value: 0.000004, currency: ai_credit } sonnet_output: { value: 0.00002, currency: ai_credit } ``` To find how many `sonnet_input` tokens 1 `ai_credit` buys: ``` 1 ai_credit / 0.000004 = 250,000 input tokens ``` --- ## Grant strategy \{#grant-strategy} When a customer has multiple grants eligible to cover overage, `grant_strategy` controls which is consumed first. | Strategy | Behavior | |---|---| | `expires_first` | The grant with the earliest expiry is consumed first. Prevents credit from going to waste. **Default.** | | `cheapest_first` | The grant with the lowest rune value per unit is consumed first. Preserves higher-value grants. | | `valuable_first` | The grant with the highest rune value per unit is consumed first. | ```yaml exchange: grant_strategy: expires_first rune: { value: 1, currency: usd } # ... ``` --- ## Multi-step chains \{#chains} Exchange chains can be arbitrarily deep. A common pattern is a two-level chain: an abstract customer-facing credit → a discrete model credit → rune. ```yaml exchange: rune: { value: 1, currency: usd } ai_credit: { value: 1.25, currency: rune } sonnet_input: { value: 0.000004, currency: ai_credit } sonnet_output: { value: 0.00002, currency: ai_credit } haiku_input: { value: 0.000001, currency: ai_credit } haiku_output: { value: 0.000005, currency: ai_credit } ``` When a customer's `ai_credit` grant is drawn against a `sonnet_input` overage, Limitr walks `sonnet_input → ai_credit → rune` to determine how much grant balance to deduct. --- ## Exchange and grants \{#exchange-and-grants} The exchange table is what makes grants work across different credit types. A grant is denominated in one credit (e.g. `ai_credit`). An entitlement limit is denominated in another (e.g. `sonnet_input`). When overage occurs on a `soft`-limit entitlement, Limitr: 1. Finds the best eligible grant per `grant_strategy` 2. Uses the exchange table to convert the overage amount from the entitlement's credit into the grant's credit 3. Deducts from the grant balance 4. Fires `meter-overage` only if no grants remain to cover the overage See [Topups & Grants](./topups) for grant lifecycle and [Enforcement](./enforcement) for the full event flow. --- ## SDK \{#sdk} ### `policy.creditExchange()` \{#creditexchange} Converts a value from one credit to another using the exchange table. Returns `null` if no path exists between the two credits. ```typescript // How many sonnet input tokens does 10 ai_credits buy? const tokens = await policy.creditExchange('ai_credit', 'sonnet_input', 10); // → 2,500,000 // Returns null if no path exists const result = await policy.creditExchange('sonnet_input', 'unrelated_credit', 1); // → null ``` --- ## Margin Per-customer margin snapshots. Limitr can compute per-customer margin snapshots without any external tooling. Because every enforcement call runs through credits with `overhead_cost` and `price` defined, Limitr knows exactly what each customer's consumption has cost you and what it's generated in revenue. :::tip[For production margin tracking] Use [Limitr Cloud](https://limitr.dev), which surfaces live per-customer margin across the full billing period — including subscription revenue, plan changes, and historical data — rather than the current in-process meter state. ::: --- ## How it works \{#how-it-works} Margin tracking is a function of how accurately you define two fields on your [discrete credits](./credits#discrete-vs-abstract): **`overhead_cost`** — What this credit costs you per unit, in runes. Your actual provider cost: what you pay Anthropic per token, AWS per GB, or your GPU provider per second. **`price`** — What you charge per unit, in runes. The amount that appears on your customer's invoice. Given these two values and the metered consumption per customer per entitlement, Limitr calculates: ``` cost = sum(overhead_cost × metered_value) per entitlement revenue = sum(price × overage) per entitlement margin = ((revenue - cost) / revenue) × 100 ``` Plan subscription revenue (flat fees) is not included in the local snapshot — that requires Cloud billing data. The local snapshot reflects usage-based margin only. --- ## `policy.customerMarginSnapshot()` \{#customermargin} Returns a live margin breakdown for a specific customer based on their current meters. ```typescript const snapshot = await policy.customerMarginSnapshot('user_abc'); ``` Return shape: ```typescript { revenue: number, // total overage revenue generated (in runes) cost: number, // total cost incurred (in runes) margin: number, // ((revenue - cost) / revenue) * 100, or -100 if no revenue entitlements: { [entitlement: string]: { cost: number, revenue: number, margin: number | null // null if no revenue on this entitlement } } } ``` ```typescript const snap = await policy.customerMarginSnapshot('user_abc'); console.log(`Revenue: $${snap.revenue.toFixed(4)}`); console.log(`Cost: $${snap.cost.toFixed(4)}`); console.log(`Margin: ${snap.margin.toFixed(1)}%`); // Per-entitlement breakdown for (const [name, ent] of Object.entries(snap.entitlements)) { console.log(`${name}: ${ent.margin?.toFixed(1) ?? 'N/A'}%`); } ``` This is a local, in-process snapshot. It reflects the current meter state in the running policy instance, not the full billing period. --- ## `policy.marginSnapshot()` \{#marginsnapshot} Projects margin for a plan given hypothetical entitlement values. Use this for pricing decisions, plan design, and margin calculators — before any real customer data exists. ```typescript marginSnapshot( plan: string, entitlements: Map ): Promise | null> ``` The `entitlements` map accepts per-entitlement values in one of two forms: **Simple value** — just the metered amount: ```typescript const projection = await policy.marginSnapshot('growth', new Map([ ['chat_input', 800000], ['chat_output', 150000], ])); ``` **Detailed map** — with explicit credit and limit: ```typescript const projection = await policy.marginSnapshot('growth', new Map([ ['chat_input', { meter: 800000, credit: 'sonnet_input', limit: 500000 }], ])); ``` When a plan is specified, Limitr resolves the credit and limit from the plan's entitlement definition automatically if not provided. The projection is scaled to the plan period by default — if an entitlement resets daily, Limitr scales it to the monthly equivalent for a monthly plan. ```typescript // { // revenue: ..., // cost: ..., // margin: ..., // entitlements: { chat_input: {...}, chat_output: {...} } // } ``` --- ## Getting margin right \{#accuracy} The snapshot is only as accurate as your `overhead_cost` values. A few things to be aware of: **Abstract credits don't contribute to margin.** Only discrete credits with `overhead_cost` defined factor into cost calculations. If an entitlement's limit is denominated in an abstract credit, it won't appear in the margin breakdown. **Margin is on overage only for soft limits.** The plan's included allocation is assumed to be covered by the subscription fee. Only consumption above the soft limit generates overage revenue in the local snapshot. **Hard-limit entitlements generate no revenue in the snapshot.** Because hard limits block overage, there's no overage billing — their cost is your cost of providing the included allocation. Use `marginSnapshot()` to model the revenue implications of a pricing change before shipping it. Use `customerMarginSnapshot()` to monitor which customers are margin-negative in production. --- ## Notifications Policy-defined event handlers. Notifications run in-process whenever a metering event occurs, letting you filter and react to specific conditions — a customer crossing 80% of their limit, a specific entitlement going into overage, a hard limit firing on a high-value customer. In Limitr Cloud, notification conditions defined in your policy also trigger managed routing to Slack, email, or webhook automatically — without changing your application code. --- ## How it works \{#how-it-works} The `notifications` block in your policy contains named `Notification` objects. Each defines a `matches()` function that filters events by type and payload, and a `fire` function that handles matching events. Limitr calls `matches(type, event)` for every metering event. If it returns `true`, `fire` is called with the event. Event types: `'meter-changed'` · `'meter-limit'` · `'meter-overage'` --- ## Defining notifications in policy (Stof) \{#stof} Notifications are defined in [Stof](https://stof.dev), the policy runtime language. The `matches` and `fire` functions are Stof functions embedded in the policy document. ```stof notifications: { high-usage-warning: { fn matches(type: str, event: obj) -> bool { type == 'meter-changed' && event.entitlement == 'chat_input' && event.meter.value >= (event.meter.limit * 0.8) } fn fire(name: str, event: obj) { // Fires in-process. Route to your app via addHandler(). // In Cloud, also triggers Cloud-managed alerting. ?App.event_handler(name, stringify('json', event)); } } hard-limit-hit: { fn matches(type: str, event: obj) -> bool { type == 'meter-limit' } fn fire(event: obj) { // Single-arg fire — name not passed ?App.event_handler('meter-limit', stringify('json', event)); } } } ``` The `fire` function can accept `(name, event)`, `(event)`, or no arguments — Limitr calls whichever signature is defined. If `fire` is not defined as a function, Limitr sends the event as a named `.send()` call using the notification's key. --- ## In-process handling with `addHandler()` \{#addhandler} `addHandler()` receives all events — both from the engine's built-in event system and from `fire` calls in your policy notifications. You don't need to write notifications in Stof to handle events in TypeScript; `addHandler()` works directly. ```typescript policy.addHandler('alerts', (key: string, value: unknown) => { const event = JSON.parse(value as string); if (key === 'meter-limit') { // Hard limit blocked a request slack.send('#alerts', `${event.customer.id} hit hard limit on ${event.entitlement}`); } if (key === 'meter-overage') { // Soft limit — overage after grants exhausted billing.queueCharge(event.customer.id, event.entitlement, event.overage); } if (key === 'high-usage-warning') { // Custom notification name fired from policy crm.flag(event.customer.id, 'approaching-limit'); } }); ``` The `key` for built-in events is always `'meter-changed'`, `'meter-limit'`, or `'meter-overage'`. The `key` for custom notifications is the notification's ID in the policy. --- ## Setting notifications at runtime \{#runtime} Notifications can be loaded into the policy at runtime without a full policy reload — useful for dynamically updating alert conditions without a redeploy. ```typescript await policy.setNotifications(notifStofString, 'stof'); ``` --- ## Event payload reference \{#payload} All events share the same base payload structure. The `value` argument in `addHandler` is a JSON string. ```typescript { customer: { id: string, plan: string, type: string, }, entitlement: string, // entitlement name plan: string, // plan ID credit: { description: string, // ... other credit fields }, meter: { value: number, // new meter value after operation limit: number, // effective limit invalid: number, // attempted value (meter-limit only) }, overage: number, // meter-overage only grant_value_applied: number, // meter-overage only } ``` See [Enforcement → Event payload shape](./enforcement#payload) for the full reference. --- ## Cloud alerting \{#cloud} In Limitr Cloud, notification conditions defined in your policy are evaluated on the Cloud side as well. When a condition matches, Cloud routes the alert to your configured channels — Slack, email, or webhook — in real time with no polling. The same enforcement event that blocks or meters the request also triggers the notification. You define the conditions once, in your policy. They fire both in-process (via `addHandler()`) and through Cloud without any additional code. --- ## `addHandler` vs policy notifications \{#choosing} | | `addHandler()` | Policy notifications | |---|---|---| | **Routing** | Your application code | Cloud-managed (Slack, email, webhook) | | **Logic language** | TypeScript | Stof | | **Deployable without code change** | No | Yes | | **Part of versioned policy** | No | Yes | | **Works without Cloud** | Yes | Yes (in-process only) | Use **`addHandler()`** when you need to react to events in application code — queue a charge, update a database, send an internal notification. Use **policy notifications** when you want Cloud-managed routing, conditions that change without a deploy, or fine-grained filtering tied to the policy's versioning workflow. --- ## Plans Named tiers that bundle entitlements for customers. Every customer is on exactly one plan. Plans define what credits customers can consume, at what limits, and which topups are available or automatically included. --- ## Schema \{#schema} ```yaml plans: : label: string # Human-facing name. Default: ''. period: string # 'monthly' | 'yearly' | 'weekly' | 'daily'. Default: 'monthly'. subscription: string # Subscription entitlement name. Default: 'subscription'. trial_period: float | string # Optional trial period (e.g. '14days'). default: bool # Is this the default plan? Default: false. hidden: bool # Hide from public plan listings. Default: false. entitlements: # Map of entitlement ID → Entitlement. : {} topups: # Map of topup ID → Topup. : {} ``` --- ## Fields \{#fields} ### `label` \{#label} Human-facing name for this plan. Available in customer state and policy serialization for display in pricing UI. ### `period` \{#period} The billing period for this plan. | Value | Duration | |---|---| | `monthly` | 30 days. **Default.** | | `yearly` | 365 days | | `weekly` | 7 days | | `daily` | 1 day | The period affects how `marginSnapshot()` scales usage for projections. It does not control meter resets — those are defined per-entitlement via `limit.reset_inc`. ### `subscription` \{#subscription} The name of the entitlement used to track plan subscription. Defaults to `'subscription'`. When `ensureCustomerPlanQuantity()` is called, Limitr increments this entitlement if the customer's meter is below 1 — useful for triggering a billing charge at plan activation. To charge for a plan subscription, define a `soft`-limit entitlement with a limit of `0` so the first increment fires a `meter-overage` event: ```yaml entitlements: subscription: limit: credit: plan_fee mode: soft value: 0 ``` ### `trial_period` \{#trial-period} Optional. A Stof duration string (e.g. `'14days'`, `'30days'`). When set, `ensureCustomerPlanQuantity()` will not increment the subscription entitlement until the trial period has elapsed since the customer was created. ```yaml trial_period: 14days ``` ### `default` \{#default} If `true`, this plan is used when `createCustomer()` is called without a plan argument. Only one plan should have `default: true`. ### `hidden` \{#hidden} If `true`, this plan is excluded from public plan serialization. Useful for internal or legacy plans. ### `entitlements` \{#entitlements} A map of entitlement IDs to `Entitlement` objects. See [Entitlements](./entitlements) for the full schema. ### `topups` \{#topups} A map of topup IDs to `Topup` objects. Topups can be applied manually with `applyCustomerTopup()` or included automatically for all customers on the plan. See [Topups & Grants](./topups) for the full schema. --- ## Example \{#example} ```yaml plans: starter: label: Starter period: monthly default: true entitlements: chat_access: description: Access to AI chat chat_input: limit: credit: sonnet_input mode: hard value: 500000 resets: true reset_inc: 1day chat_output: limit: credit: sonnet_output mode: hard value: 200000 resets: true reset_inc: 1day growth: label: Growth period: monthly entitlements: chat_access: description: Access to AI chat chat_input: limit: credit: sonnet_input mode: soft value: 700000 resets: true reset_inc: 1day chat_output: limit: credit: sonnet_output mode: soft value: 400000 resets: true reset_inc: 1day topups: ai_credit_pack: description: 10 AI credits credit: ai_credit value: 10 price: amount: 12.50 ``` --- ## SDK \{#sdk} ### `policy.plan()` \{#plan} Returns the plan record for a plan ID or customer ID. Falls back to the default plan if `def` is `true` (default) and the specified plan isn't found. ```typescript const plan = await policy.plan('growth'); const customerPlan = await policy.plan('user_123'); // looks up customer's plan ``` ### `policy.defaultPlan()` \{#defaultplan} Returns the plan marked `default: true`, if one exists. ```typescript const plan = await policy.defaultPlan(); ``` ### `policy.setPlan()` \{#setplan} Adds or replaces a plan in the policy by ID. `planStof` is a Stof string defining the plan. Fires a `plan-set` event. ```typescript await policy.setPlan('enterprise', planStofString); ``` ### `policy.deletePlan()` \{#deleteplan} Removes a plan by ID. Fires a `plan-removed` event. Returns `true` if removed. ```typescript await policy.deletePlan('legacy'); ``` ### `policy.planPeriod()` \{#planperiod} Returns the period string for a plan. ```typescript const period = await policy.planPeriod('growth'); // 'monthly' ``` ### `policy.planTrialPeriod()` \{#plantrialperiod} Returns the trial period in milliseconds, or `null` if no trial is defined. ```typescript const trialMs = await policy.planTrialPeriod('starter'); // null or ms value ``` ### `policy.planSubEntitlementName()` \{#plansubentitlementname} Returns the subscription entitlement name for a plan. ```typescript const name = await policy.planSubEntitlementName('growth'); // 'subscription' ``` ### `policy.setCustomerPlan()` \{#setcustomerplan} Changes a customer's plan. When `overwrite_meters` is `true` (default), resets the customer's meters. Returns `true` if the plan changed, and fires `customer-set` and `customer-plan-changed` events. ```typescript await policy.setCustomerPlan('user_123', 'growth'); ``` --- ## Policy # Policy Reference A single document for your entire pricing & enforcement. The policy is the source of truth for everything Limitr does at runtime — enforcement, metering, margin, and alerting. This section documents every field, type, constraint, and behavior. --- ## What a policy is A Limitr policy is a document. You write it in YAML, JSON, TOML, or [Stof](https://stof.dev). At runtime it's parsed into the `Limitr` type — a WebAssembly-powered object that runs in-process with your application. There are no network calls on the enforcement path. The top-level structure: ```yaml policy: credits: {} # Credit definitions exchange: {} # Exchange table plans: {} # Plan definitions (entitlements, topups) notifications: {} # In-policy event handlers (optional) customers: {} # Customer state (managed by the SDK) capabilities: {} # Callable policy logic (Cloud/Network) ``` You define `credits`, `exchange`, `plans`, and `notifications`. The SDK manages `customers`. `capabilities` are an advanced Cloud and Limitr Network concept. --- ## Loading a policy ### Local (open source) ```typescript // From a file const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml'); // From a string const policy = await Limitr.new(` policy: credits: seats: overhead_cost: 0 `, 'yaml'); // Supported formats: 'yaml' | 'json' | 'toml' | 'stof' ``` `Limitr.new()` initializes WebAssembly and validates the policy before returning. Throws on validation failure. ### Cloud (managed) ```typescript const policy = await Limitr.cloud({ token: 'limitr_...' }); ``` Your policy lives in the Cloud dashboard. The SDK connects and syncs it automatically. The `policy.allow()` call — and every other enforcement call — is identical to local. Nothing in your application code changes. --- ## Validation Every `Limitr.new()` call validates the policy by default. To skip validation (not recommended in production): ```typescript const policy = await Limitr.new(doc, 'yaml', false); // third arg: validate ``` To validate manually after loading: ```typescript const [valid, error] = await policy.valid(); if (!valid) console.error(error); ``` --- ## Serializing the policy document The policy document is fully serializable at any time. Useful for driving pricing UI, syncing state, or inspecting the policy at runtime. ```typescript const json = policy.doc.stringify('json'); const yaml = policy.doc.stringify('yaml'); const toml = policy.doc.stringify('toml'); const obj = policy.doc.record(); // plain JS object ``` --- ## Reference pages | Page | What it covers | |---|---| | [Credits](./credits) | Credit types, pricing models, units, overhead cost and price | | [Exchange](./exchange) | Rune system, exchange pairs, grant strategy, credit conversion | | [Plans](./plans) | Plan structure, billing period, subscription, trial period | | [Entitlements](./entitlements) | Entitlement and limit types, modes, resets, scope, overrides | | [Customers](./customers) | Customer type, refs, alt IDs, overrides, metadata | | [Topups & Grants](./topups) | Topup definitions, grant lifecycle, reset modes, rollover | | [Enforcement](./enforcement) | allow, increment, decrement, check, value, remaining, events | | [Margin](./margin) | customerMarginSnapshot, marginSnapshot, overhead_cost and price | | [Notifications](./notifications) | Notification type, event matching, in-process and Cloud routing | --- ## Topups & Grants Credit packages defined on a plan. When applied to a customer, a topup creates a **grant** — a credit balance on that customer. Grants are consumed before `meter-overage` events fire on `soft`-limit entitlements, letting you buffer overage against purchased or included credit rather than immediately billing. Grants are also consumed before `meter-limit` events on `hard`-limit entitlements, enabling pure credit burndown strategies without a hard stop. --- ## Topup schema \{#topup-schema} Topups are defined inside a plan's `topups` map. ```yaml plans: growth: topups: : description: string # Optional. credit: string # Required. Credit ID granted. value: float|string # Required. Amount of credit granted. price: # Optional. What this topup costs. amount: float included: bool # Auto-apply to all customers on this plan? Default: false. included_scopes: [string] # Limit auto-apply to specific customer types. Default: all. resets: bool # Does the grant reset? Default: false. reset_inc: duration # Duration-based reset interval. Mutually exclusive with reset_sch. reset_sch: string # Calendar reset schedule. Mutually exclusive with reset_inc. reset_mode: string # 'hard' | 'add' | 'rollover'. Default: 'hard'. rollover_min: float|string # Minimum carry-forward (rollover mode). rollover_max: float|string # Maximum carry-forward (rollover mode). rollover_pct: float # Carry-forward percentage (rollover mode). max_balance: float|string # Cap on grant balance after reset. expires_after: duration # Grant expires N ms after purchase. reset_catchup_cap: int # Max catch-up resets if periods are missed (duration resets only). ``` --- ## Topup fields \{#fields} ### `credit` \{#credit} The credit ID this topup grants. Can be abstract or discrete. When abstract, the [exchange table](./exchange) governs how the grant is drawn against entitlement overages. ### `value` \{#value} How much credit is granted, expressed in the credit's `stof_units`. Must be > 0. ### `price` \{#price} What this topup costs in runes. If `null`, the topup is free. Used by Cloud billing when a customer purchases a topup. ### `included` \{#included} If `true`, the topup is automatically applied to all new customers on this plan, and to existing customers when `ensureCustomerIncludedTopups()` is called. The customer gets the grant without purchasing it. ### `included_scopes` \{#included-scopes} Limits auto-application of an included topup to specific customer types (e.g. only `'user'`, not `'org'`). Leave `null` to apply to all types. ### `resets`, `reset_inc`, and `reset_sch` \{#resets} If `resets` is `true`, the grant resets on a schedule. Use either `reset_inc` for a fixed duration or `reset_sch` for a calendar boundary — the two are mutually exclusive. If neither is set, `reset_inc` defaults to `30days`. **Duration-based (`reset_inc`)** — resets N ms after the grant was last reset. Each customer resets at a different wall-clock time depending on when their grant was created. ```yaml resets: true reset_inc: 30days # resets 30 days after last reset ``` **Calendar-based (`reset_sch`)** — resets on a calendar boundary, regardless of when the grant was created. The right choice for plan-included allocations that should align to a billing cycle. ```yaml resets: true reset_sch: 'monthly:1' # 1st of every month reset_sch: 'monthly:last' # last day of every month reset_sch: 'weekly:mon' # every Monday reset_sch: 'nth_weekday:1:tue' # first Tuesday of every month ``` All schedules use UTC. :::note[Calendar resets don't catch up] Duration-based resets apply catch-up periods for missed intervals (controlled by `reset_catchup_cap`). Calendar resets always fire exactly once per period — there is no catch-up for missed periods. ::: ### `reset_mode` \{#reset-mode} Controls what happens to the grant balance at reset time. | Mode | Behavior | |---|---| | `hard` | Resets balance to `starting_value`. Unused credit is discarded. **Default.** | | `add` | Adds `starting_value` to the current balance. Credit accumulates over time. | | `rollover` | Carries the current balance forward (with optional decay and clamping), then adds the new allocation. See `rollover_*` fields. | ### `rollover_min`, `rollover_max`, `rollover_pct` \{#rollover} Used with `reset_mode: rollover`. Applied in this order at reset time: 1. Multiply carried balance by `rollover_pct` (e.g. `0.5` = carry 50% of unused credit) 2. Clamp result to `[rollover_min, rollover_max]` 3. Add `starting_value` 4. Clamp total to `max_balance` ### `max_balance` \{#max-balance} A cap on the total grant balance after reset. Prevents accumulation from exceeding a ceiling. ### `expires_after` \{#expires-after} The grant created by this topup expires N milliseconds after it was granted. Expired grants are discarded automatically on the next enforcement operation that checks them. ### `reset_catchup_cap` \{#reset-catchup-cap} For duration-based resets only. If a grant is multiple periods behind — for example, the policy hasn't been loaded for 60 days and the reset is monthly — Limitr normally calls `reset_period()` for each missed period. This field limits how many times it will do so. Set to `1` to never accumulate missed periods. Not applicable to calendar-based resets (`reset_sch`), which always reset exactly once per period. --- ## Grant schema \{#grant-schema} Grants are created automatically from topups. You don't define them in the policy. The `Grant` object on a customer has: ```typescript { id: string, // Grant ID ('lgrnt_...') credit: string, // Credit ID topup: string, // Topup name that created this grant (if any) created_on: number, // Timestamp (ms) — when the grant was created granted_on: number, // Timestamp (ms) — anchor for duration-based resets starting_value: number, // Value at grant creation (used for resets) value: number, // Current balance resets: boolean, reset_inc: number | null, // ms, or null when reset_sch is used reset_sch: string | null, // calendar schedule, or null when reset_inc is used last_reset: number | null, // timestamp of last calendar reset (reset_sch only) expires_on: number | null, // ms, or null reset_mode: string, // ... rollover fields } ``` --- ## How grants cover overage \{#grant-overage} When a `soft`-limit entitlement goes into overage: 1. Limitr checks the customer's grants for any that can cover the overage credit (directly or via the exchange table). 2. It selects the best grant per the exchange [`grant_strategy`](./exchange#grant-strategy). 3. It converts the overage amount from the entitlement's credit into the grant's credit and deducts from the grant balance. 4. If the grant fully covers the overage, `meter-overage` does not fire. 5. If grants are exhausted or insufficient, `meter-overage` fires with the remaining uncovered amount in `event.overage`. A grant with `resets: false` that reaches `value: 0` is automatically removed. --- ## Examples \{#examples} ### Monthly included credit allocation (calendar-aligned) ```yaml topups: monthly_credits: description: 100 AI credits included monthly credit: ai_credit value: 100 included: true resets: true reset_sch: 'monthly:1' # resets on the 1st of every month reset_mode: hard # unused credits don't carry over ``` ### Monthly included credit allocation (duration-based) ```yaml topups: monthly_credits: description: 100 AI credits included monthly credit: ai_credit value: 100 included: true resets: true reset_inc: 30days # resets 30 days after the grant was last reset reset_mode: hard ``` ### Purchasable credit pack with expiry ```yaml topups: ai_boost_pack: description: 500 AI credits, expires in 90 days credit: ai_credit value: 500 price: amount: 49.00 expires_after: 90days ``` ### Rollover credits (carry 50% of unused) ```yaml topups: rollover_pack: credit: ai_credit value: 100 included: true resets: true reset_inc: 30days reset_mode: rollover rollover_pct: 0.5 # carry 50% of unused balance rollover_max: 150 # never carry more than 150 credits max_balance: 250 # total balance can't exceed 250 ``` --- ## SDK \{#sdk} ### `policy.applyCustomerTopup()` \{#applycustomertopup} Applies a topup from the customer's plan to the customer, creating a grant. Returns `true` if successful. ```typescript await policy.applyCustomerTopup('user_abc', 'ai_boost_pack'); ``` ### `policy.ensureCustomerIncludedTopups()` \{#ensurecustomerincludedtopups} Ensures all `included` topups on the customer's plan are applied, and removes grants for topups that are no longer applicable. Call this after a plan change or on customer load. :::note[Called automatically] This is called automatically when creating new customers and when adding customers from Limitr Cloud. To control which included topups apply to which customers, use the `included_scopes` field on the topup. ::: ```typescript await policy.ensureCustomerIncludedTopups('user_abc'); ``` ### `policy.remainingCredit()` \{#remainingcredit} Returns the total remaining balance of a specific credit across all of a customer's grants, after exchange conversion. This is grant balance only — it does not include entitlement meter state. ```typescript const balance = await policy.remainingCredit('user_abc', 'ai_credit'); // Total ai_credit remaining across all grants on this customer ``` --- ## How to Design a Pricing Model That's Flexible and Scalable for AI Products We hear a version of the same story over and over in customer calls. A founder has a usage-based product in the market. They have some customers. Their pricing "works" — in the sense that money is coming in and the product is shipped. But when you ask how they're tracking usage, enforcing limits, or calculating margins? The answer is almost always some version of: "We build an in-house solution that works for now". That's not a failure. That's just how early-stage companies survive. You hack things together. You make it work. But at some point, usually right before a major product launch or a new set of features hits the roadmap, that manual scaffolding starts to crack. This post is about how to think through pricing before it becomes a crisis. What the data says. What we're seeing from companies in the field. And what it actually takes to build a model that can grow with you. {/* truncate */} ## Why Pricing Keeps Getting Harder for AI Companies Traditional SaaS had it relatively easy. Your marginal cost per user was close to zero. You could charge a flat monthly fee, set your tiers, and call it a day. AI products don't work that way. Every API call costs money. Every model inference burns compute you're paying for. Every token processed is a line item on your cloud bill. Your cost structure is fundamentally consumption-based — which means your pricing needs to be, too. And yet most AI founders are still trying to force those variable costs into fixed pricing models. The result? Margin erosion, pricing confusion, and customers who are frustrated because their bill doesn't match what they expected. According to OpenView Partners, 61% of new B2B SaaS products are now exploring usage-based pricing — but adoption is still lagging behind where the market is heading. And a16z is clear: AI is actively driving a shift away from pure subscription models toward usage-based and outcome-based pricing. The companies that figure this out early have a serious competitive advantage. The ones that don't end up renegotiating with every enterprise customer, eating overages, and scrambling to fix their billing setup right before a big launch. ## Three Models — and What Each One Is Actually For ### 1. Pure Subscription You charge a flat fee per month (or per seat). Customers know exactly what they're paying. Sales is easier. Revenue is predictable. The problem: if your costs are variable, your margins aren't. You end up subsidizing your heaviest users and undercharging your lightest ones. At scale, this becomes a profitability problem. When it works: Very early stage, when you're still learning your usage patterns. Or for B2C products where simplicity of pricing is a purchasing decision driver. ### 2. Usage-Based Pricing Customers pay for what they use. You align revenue with value delivered. Margins hold up even as usage scales. The problem: customers hate unpredictable bills. Sales gets harder. Finance teams at bigger companies struggle to budget for consumption-based software. And you need real infrastructure to track, enforce, and bill for usage accurately. When it works: Infrastructure and API products, dev tools, any product where the user is another piece of software (not a person clicking around). ### 3. Hybrid Pricing — Where Most AI Companies End Up A base platform fee (predictability for the customer) plus usage-based charges above a threshold (protection for your margins). Often layered with credits, top-up options, and enterprise-level overrides. According to a16z's framework, usage-based pricing tends to work best when the end user is software — not a human. For human-facing products, pure subscription is often cleaner. But for most AI products today, the right answer is somewhere in the hybrid middle. OpenView's benchmark data shows that 86% of SaaS companies valued above $100M use at least three dimensions in their pricing structure, and companies running multi-dimensional models show 34% higher LTV/CAC ratios than those using simpler models. The most popular structure in 2025, per multiple industry reports: a monthly base fee, a credit pool, overages billed as needed. It's not revolutionary. But executing it well — especially as your product evolves — is harder than it sounds. ## What We're Actually Seeing in the Field We talk to founders at usage-based companies every week. A few patterns show up constantly. The "back of the napkin" problem. A founder building an integration-heavy SaaS product told us recently that their pricing setup is "just a lot of manual calculators." Their team tracks usage through a basic tool, the founder needs a special login from their co-founder to see the metadata, and there's no clear dashboard showing per-customer margin. It works — until it doesn't. They're planning to launch two new add-on features in Q3, which will open new markets and make usage data far more important. The manual setup won't survive that. The tier trap. Several companies we've spoken with built what felt like a solid pricing structure — usage buckets with auto-bumps after consecutive overages. Logical. Simple. Explainable. And then a couple of enterprise prospects came in with completely different requirements: percentage-of-contract-value pricing, custom usage thresholds, per-customer exceptions. The existing structure couldn't handle it without manual overrides for every deal. The "eating it" overage problem. When customers blow through their usage limits — especially unexpectedly — someone has to absorb the cost. For most early-stage companies, that means the founder eats it, then figures out what to add to the contract going forward. One company we spoke with had an overage "significant enough" before finally building protective clauses into their customer contracts. The pricing lag. Bigger companies — Notion, Salesforce, Miro — can introduce AI features and leave them un-monetized for a year while they learn usage patterns. They're treating early AI adoption as a loss leader. Early-stage companies can't do that unless they have a war chest. Which means you need to understand your usage data before you commit to a pricing model — not after. ## How to Actually Design a Flexible, Scalable Pricing Model Here's the framework we've built [Limitr](https://limitr.dev) around, and what we recommend to every founder we work with. ### Step 1: Start in Observe Mode Before you commit to a scalable pricing structure, you need real usage data. What features are your customers actually using? Which ones are driving cost? What's the distribution of usage across your customer base — do a few heavy users account for most of your infrastructure spend? You can't answer these questions without instrumentation. And most early-stage companies don't have it. The approach that works: instrument your product to track usage at the feature level before you monetize it. Know what a "unit" of value looks like in your product. Know what it costs you. Then build your pricing around that. ### Step 2: Design Your Pricing as a Policy, Not a Codebase The biggest scaling problem with pricing is that most teams bake it into their code. A new tier means a new release. A limit change means a hotfix. An enterprise exception means a custom branch. That's a nightmare. Pricing should be a configuration you can change in minutes — not a deployment you have to schedule. The mental model that works: think of your pricing as a document. One document that says: who can access what, up to what limits, at what cost, with what overrides. Any change to that document should propagate immediately to your product, your billing, and your customer-facing UI — without touching your codebase. ### Step 3: Build for the Customer You Don't Have Yet Early customers often accept rough pricing because they like you and they want the product to work. Enterprise customers — or any customer who's actually going to scrutinize the contract — won't. You need to be able to handle: - Custom limits per customer - Discounts and promotional pricing - Soft limits with overage billing vs. hard limits with denial - Top-up credits for customers who want to buy more - Per-feature pricing as your product expands If your pricing infrastructure can't do any of those things in minutes, you'll lose deals or you'll paper over them with manual exceptions that compound over time. ### Step 4: Align Your Pricing Unit with Your Value Unit This sounds obvious but most teams get it wrong. Your pricing unit should map to the thing your customer cares about — not the underlying cost to you. If you're a customer success tool, don't charge per API call. Charge per customer interaction, or per seat, or per outcome. If you're a developer tool, tokens or compute units might actually map well. If you're integration infrastructure, active integrations or syncs per month is probably closer to the value. The rule of thumb from a16z: if your end user is a human, pricing by usage is harder to sell. If your end user is software, consumption pricing is more natural and expected. ### Step 5: Don't Freeze Your Pricing Before You Know Your ICP One insight that comes up repeatedly when talking to pricing experts: most early-stage companies lock in a pricing model before they've found their true ICP. Then they have to renegotiate with every enterprise customer because the model doesn't fit their purchase process. The solution: build pricing infrastructure that's flexible enough to let you run experiments — different tiers for different segments, pilot pricing for design partners, outcome-based pricing for high-value deals — without requiring engineering work every time. Pricing is a product decision. It should be treated like one. ## The Cost of Getting This Wrong If you're thinking "we'll figure this out later" — here's what "later" actually costs. **Margin erosion.** Heavy users on flat pricing tiers are often unprofitable. Without per-customer cost visibility, you won't know until you do the math — and by then you've made commitments you can't easily unwind. **Enterprise deals lost.** Enterprise buyers often have procurement requirements that flat-tier consumer-style pricing can't satisfy. If you can't offer custom limits, volume discounts, or usage-based commitments, you'll lose deals to competitors who can. **Engineering tax.** Every pricing change that requires a code change is engineering time that didn't go toward the product. This compounds. Teams that treat pricing as infrastructure from the start spend dramatically less time firefighting. **Forecasting blindness.** If you don't know your per-customer margin, you can't model your business accurately. You can't tell investors a credible story about unit economics. You can't price your next feature correctly. ## The Bottom Line Pricing flexibility isn't a nice-to-have. For AI companies — where your cost structure is inherently variable — it's a core part of your product infrastructure. The companies getting this right aren't doing anything exotic. They're starting with real usage data. They're treating pricing like a config, not a codebase. They're building enforcement and billing into their product from the start, rather than bolting it on after they've already made promises they can't keep. The companies getting this wrong are doing a lot of back-of-the-napkin math, manual Stripe configurations, and praying their heavy users don't notice their margins. You already know which camp you want to be in. --- Sources: OpenView Partners — State of Usage-Based Pricing (https://openviewpartners.com/blog/state-of-usage-based-pricing/) | a16z — AI Is Driving a Shift Towards Outcome-Based Pricing (https://a16z.com/newsletter/december-2024-enterprise-newsletter-ai-is-driving-a-shift-towards-outcome-based-pricing/) | a16z — Usage-Based Pricing: Our Rule of Thumb (https://a16z.com/usage-based-pricing-rule-of-thumb/) | a16z — Pricing & Packaging Your AI Product (https://a16z.com/pricing-packaging-ai-b2b-prosumer/) | Metronome — AI Pricing in Practice: 2025 Field Report (https://metronome.com/blog/ai-pricing-in-practice-2025-field-report-from-leading-saas-teams) --- ## AI Pricing Strategy Guide for 2026: How to Cut Engineering Costs and Drive Revenue Growth If you're building an AI product in 2026, you're likely managing a cost structure that your pricing model was never designed for. Traditional SaaS pricing — flat tiers, per-seat subscriptions — was built for near-zero marginal cost software. It made sense when the cost to serve a new user was effectively nothing. AI and agentic products don't work that way. Every model inference costs money. Every API call has a price. Your most active customers might cost you 10x what your least active ones do. {/* truncate */} The result? Margin erosion that's invisible until it's material. Pricing structures that break the moment you land a large customer or ship a new feature. Engineering teams spending 25–40% of their cycles on billing logic instead of building the actual product. This guide exists to help you avoid that. Whether you're pre-revenue designing your first pricing model, or Series B+ cleaning up years of pricing debt — the playbook is the same: get real usage data first, build pricing as infrastructure (not code), and charge for the value you actually deliver. ## 1. The State of AI Pricing in 2026 The pricing landscape has shifted dramatically in the past 18 months. AI is no longer an add-on feature. It's the product. That means pricing AI correctly isn't a nice-to-have — it's survival math. The numbers tell the story: - 61% of new B2B SaaS products are now exploring usage-based pricing, up from 49% just a year ago (OpenView Partners: https://openviewpartners.com/blog/state-of-usage-based-pricing/) - 80% of enterprises miss their AI cost forecasts by more than 25% (Mavvrik AI Cost Governance Report: https://www.mavvrik.ai/ai-cost-governance-report/) - 84% of companies report significant gross margin erosion tied to AI workloads — and for companies where 50%+ of customers use AI features, that margin hit reaches 16% (Mavvrik) - Companies using per-seat pricing for AI products report 40% lower gross margins and 2.3x higher churn than those on usage-based or hybrid models (Bessemer Venture Partners: https://www.bvp.com/atlas/the-ai-pricing-and-monetization-playbook) - 86% of SaaS companies valued above $100M use at least three dimensions in their pricing structure, and those companies show 34% higher LTV/CAC ratios (OpenView Partners) What's happening is a full-category repricing event. The companies that get ahead of it now will have a structural advantage over those scrambling to retrofit pricing later. **Why this hits early-stage companies hardest:** Large, well-funded companies can absorb AI feature losses as a growth investment — treating early AI adoption as a loss leader while they figure out the right model. Early-stage startups can't do that without significant runway. If your per-seat pricing is subsidizing heavy AI users, you're funding their usage out of your capital. That's not a business model problem. It's a pricing infrastructure problem. And it has a concrete solution. ## 2. Three Pricing Models Every AI Startup Needs to Understand There's no universally "correct" AI pricing model. There are more out there than these, but we'll focus on three frameworks every AI founder needs to understand — and a clear set of signals that tells you which one you actually need right now. **Model 1: Pure Subscription (Flat Tiers)** You charge a fixed monthly fee per seat or per plan tier. Simple. Predictable. Easy to sell. The problem: if your underlying costs are variable — and for AI products, they always are — flat pricing creates margin instability. Your lightest users are highly profitable. Your heaviest users may be actively unprofitable. You're averaging across them, which masks the problem until it's too late. When it works: Pre-revenue, when you're still learning your cost structure. Or for B2C products where pricing simplicity drives conversion. Or when AI is a small, incidental feature rather than the core value driver. **Model 2: Usage-Based Pricing (Consumption)** Customers pay for what they use. Revenue scales with value delivered. Margins stay consistent because costs and revenue move together. The problem: unpredictable bills create friction in sales and procurement. Enterprise finance teams struggle to budget for variable software spend. And you need real metering infrastructure to do this right — which most early-stage companies don't have yet. When it works: API products, developer tools, infrastructure — any product where the end user is software rather than a human. According to a16z (https://a16z.com/usage-based-pricing-rule-of-thumb/), usage-based pricing fits naturally when the buyer is technical and the unit of value maps cleanly to consumption. **Model 3: Hybrid Pricing (Base Fee + Usage)** A fixed platform fee gives customers cost predictability, while usage-based components protect your margins and align revenue to actual value delivered. The base fee covers access, support, and SLA commitments. The usage component captures the incremental value of heavy usage — model calls, API hits, seats, or AI credits. This is where most AI companies ultimately land. OpenView Partners data shows the most popular structure in 2025 is: monthly base fee + a credit pool + overages billed as needed. When it works: Series A and beyond, when you have enough usage data to set limits intelligently and enough deal volume to see patterns across your customer base. **What most companies get wrong:** They choose a model before they have data. They lock in tiers based on intuition, launch, and then discover that 20% of customers are generating 80% of their infrastructure costs. Renegotiating is painful. Repricing is a churn risk. The better path: start collecting usage data on day one, even before you monetize it. When you have the data, the right pricing model becomes obvious. ## 3. The Engineering Cost of Getting Pricing Wrong Most founders think about pricing mistakes in terms of revenue left on the table. The more immediate cost is usually operational — specifically, engineering time. **The hidden engineering tax** When pricing logic lives in code, every pricing change is an engineering task: - New tier? New release cycle. - Enterprise exception? Custom branch. - Overage rule? Someone needs to write it, test it, and deploy it. - Customer wants a usage dashboard? Another sprint. This is the default state at most early-stage AI companies — and the data makes the cost visible: - Companies building billing and enforcement infrastructure in-house typically allocate 25–40% of engineering resources to billing-related work (Chargebee: https://www.chargebee.com/blog/ai-monetization-billing-infrastructure/) - Building metering, enforcement, and customer billing dashboards to production quality can take 6–12 months and costs over $200K in engineering time - Companies that regularly review and optimize their pricing see 30% higher growth rates than those that don't (OpenView Partners) Every week your engineering team spends on pricing infrastructure is a week they're not building product. **What "pricing debt" looks like in practice:** In conversations with usage-based founders, the same story surfaces repeatedly. A founder is doing back-of-the-napkin math to calculate margins per deal. They need a special login from their CTO to see usage metadata. This isn't a sign of a poorly-run company. It's a sign of a company that moved fast to get customers — and deferred the infrastructure problem. But at some point, usually right before a new product launch or a new sales motion, the manual scaffolding cracks. An enterprise prospect asks: "Can you do custom limits, credit bundles or topups?", and suddenly the manual setup can't act fast enough. **The compounding cost of waiting:** - Margin erosion: At 16% margin erosion, a $20M ARR company loses $3.2M in annual gross profit it could have kept. - Forecasting blindness: Without per-customer cost visibility, you can't identify which customers are profitable, which are loss leaders, or when to trigger upsell conversations. - Enterprise deals lost: Custom pricing requests you can't fulfill fast enough cost deals. The longer your pricing change cycle, the more leverage buyers have. - Churn from model mismatch: 2.3x higher churn for companies on misaligned pricing models compounds over time. ## 4. Five Steps to a Flexible, Scalable Pricing Model Here's the framework — and what we'd recommend to any AI founder working through this for the first time or the fourth time. **Step 1: Start in Observe Mode Before You Monetize** Before you charge for usage, measure it. Add instrumentation to understand what features customers use, how often, and at what cost to you. This data is the foundation of every pricing decision you'll make. Most teams skip this step because they're in a hurry. Don't. The companies that have this data design pricing with confidence. The ones that don't are guessing. Things to track from day one: - Feature-level usage per customer - Cost per unit of value (per API call, per model inference, per active session) - Distribution of usage across your customer base — how top-heavy is it? - Which features drive retention vs. which are rarely touched **Step 2: Design Pricing as a Policy, Not a Deployment** Your pricing model should live in a single, versioned configuration — not scattered across hundreds of code files. That configuration should be readable by GTM and Finance, not just engineers. And it should be updatable without a code release. The mental model: imagine a document that says exactly who gets access to what, at what limits, with what pricing rules. Every change to that document propagates instantly to your product, your billing, and your customer-facing UI. When pricing lives in code: changes take weeks, exceptions become technical debt, GTM can't move without engineering. When pricing lives in a policy document: changes take minutes, exceptions are configurations, and Finance and Product can own pricing decisions directly. **Step 3: Build for Customers You Don't Have Yet** Early customers are forgiving. Enterprise customers aren't. Build your pricing infrastructure to handle what a demanding enterprise buyer will ask for — before you're in the room with them. Your pricing model should be able to support: - Custom per-customer limits, both soft (invoice for overage) and hard (deny any overage) - Overage billing vs. hard cutoffs, configurable per plan - Volume discounts and promotional pricing - Top-up credits and add-on features - Real-time usage visibility for customers, not just your team - Audit trail of pricing changes over time If your current setup can't do any of these things in under 10 minutes, you'll lose enterprise deals or paper over them with manual exceptions that compound into maintenance nightmares. **Step 4: Align Your Pricing Unit to Your Value Unit** Your pricing unit — the thing you charge for — needs to match the unit of value your customer actually experiences. This is the hardest step and the most important one. A few examples: - Customer success platform → charge per customer interaction, not per API call - AI writing tool → charge per document or words generated, not per token - Integration infrastructure → charge per active sync or integration, not per request - Developer API → tokens or compute units are natural and expected The test: if a customer can explain your pricing in one sentence, and it maps to how they think about the value they receive, you have the right unit. If they have to understand your cost structure to understand your pricing, you don't. **Step 5: Test Pricing Without Engineering Sprints** Pricing is a product decision. It should be iterated on like one — quickly, with data, without requiring engineering intervention every time. This means: - Testing different limits for different customer segments - Piloting new tier structures with a subset of customers - Moving a customer to a new plan in minutes, not days - Reverting pricing changes if you make a mistake OpenView data shows a 30% higher growth rate for companies that regularly optimize pricing. The constraint for most teams isn't strategy — it's the time and cost of making pricing changes. ## 5. Stage-by-Stage Pricing Playbook What pricing decisions matter most depends on where you are in your company's growth. Here's how to think through each stage. **Pre-Seed and Seed (0–$500K ARR)** Your primary goal isn't optimizing pricing. It's learning. - Use flat-tier or founder-negotiated pricing to close your first 10–20 customers - Instrument everything: add usage tracking from day one, even if you're not charging for it yet - Don't build billing infrastructure from scratch — use Stripe's defaults and reconcile manually if needed - Identify your pricing unit: what one thing do your best customers have in common in terms of usage? - Avoid long-term contracts that lock in pricing before you understand your cost structure **Series A ($500K–$5M ARR)** You have customer data. Use it. - Review per-customer margin for every account — if any are unprofitable, understand exactly why - Begin transitioning toward hybrid pricing: a base fee plus usage-based components - Build or buy metering infrastructure — this is no longer optional at this stage - Define your standard pricing tiers, but build in flexibility for enterprise exceptions - Instrument your pricing model so non-engineers can make changes without a deployment **Series B+ ($5M+ ARR)** Pricing is a growth lever, not just a billing question. - Run structured pricing experiments: A/B test tiers, limits, and packaging for new segments - Expand to outcome-based components for enterprise deals where value is clearly measurable - Build customer-facing usage dashboards — enterprise buyers want visibility into their own spend - Connect pricing changes to revenue impact: if you increase a limit, what's the ARR effect? - Review pricing quarterly — every major AI model release is a potential re-pricing event ## 6. Metrics That Actually Matter Most pricing conversations focus on revenue metrics. The ones that actually tell you whether your model is working are cost metrics. **Gross margin by customer segment** Not just overall gross margin. Break it down by plan, usage tier, and customer cohort. If your enterprise customers are less profitable than your SMB customers, that's a pricing problem, not a sales problem. **Usage distribution** How concentrated is usage in your top 10% of customers? If your top decile accounts for more than 60% of compute costs, you likely have a pricing structure problem. **Time to pricing change** How long does it take from "we want to change this limit" to the change going live in production? If the answer is more than one business day, your pricing is baked into your codebase. **Overage rate** What percentage of customers are regularly exceeding plan limits? High overage rates signal either repricing opportunity (limits are too low) or churn risk (customers would leave if you enforced them). **Pricing exception rate** How often do you make manual per-customer pricing exceptions? More than 20% is a sign your standard pricing model doesn't fit your ICP. ## Frequently Asked Questions **What's the right time to switch from flat-tier to usage-based pricing?** When you have enough usage data to set limits intelligently and when flat-tier subsidies are materially affecting your gross margin. For most teams, this happens somewhere between 20–50 customers. **How do you handle enterprise customers who want custom pricing?** Custom pricing should be a configuration, not a coding project. If making an exception for an enterprise customer requires an engineering ticket, you need better pricing infrastructure. The goal is to offer any customer any limit, discount, or usage structure in minutes. **Should we build our own billing infrastructure?** No. Building metering, enforcement, and billing infrastructure in-house is a short-term fix. The ROI almost never pencils out for companies under $10M ARR. Use infrastructure built for this purpose. **How should we think about AI pricing as model costs drop over time?** Your pricing model should abstract away from model costs entirely. Customers should pay for value received, not for your cost to deliver it. When model costs fall 50%, use that to improve margins or fund new features — don't automatically pass it through. **What's the single biggest mistake founders make with pricing?** Locking in a pricing structure before understanding usage patterns. The companies that get this right spend 2–3 months observing usage before they monetize it. The ones that get it wrong price on intuition, then spend 12–18 months unwinding structures that don't fit their actual customer base. --- **Sources** OpenView Partners — State of Usage-Based Pricing: https://openviewpartners.com/blog/state-of-usage-based-pricing/ Mavvrik — AI Cost Governance Report: https://www.mavvrik.ai/ai-cost-governance-report/ Bessemer Venture Partners — AI Pricing and Monetization Playbook: https://www.bvp.com/atlas/the-ai-pricing-and-monetization-playbook a16z — Usage-Based Pricing Rule of Thumb: https://a16z.com/usage-based-pricing-rule-of-thumb/ a16z — AI Is Driving a Shift Towards Outcome-Based Pricing: https://a16z.com/newsletter/december-2024-enterprise-newsletter-ai-is-driving-a-shift-towards-outcome-based-pricing/ --- ## We Mean Runtime Literally Everyone in usage-based pricing says enforcement has to happen at runtime. We agree. But that word is doing two different jobs, and the difference matters more than it looks like it should. *At runtime* is about timing. The check happens while your code runs, instead of in a nightly job or at month-end. *A runtime* is a thing. An execution environment that loads code and runs it. We mean the second one. Your pricing policy is a document with logic in it, and that logic runs inside your process. Not a fast API call. Not a cached copy of your limits. The policy itself, running where your code runs. {/* truncate */} ## What embedded actually means Limitr policies are written in [Stof](https://stof.dev), an open-source data runtime that's a superset of JSON plus one thing that matters here: a document can carry functions, not just fields. So a policy isn't config that our engine interprets according to its own rules. It's a document holding both the numbers and the logic that acts on them. ```rust policy: { plans: { pro: { label: "Pro Plan" entitlements: { analytics: {} } } } customers: { john: { plan: "pro" } } fn has_analytics(id: str) -> bool { const customer = self.customers.get(id); const plan = self.plans.get(customer.plan ?? 'pro'); ?plan.entitlements.contains('analytics') } } ``` The Stof runtime is Rust compiled to [WebAssembly](https://webassembly.org/). Limitr embeds it in your application, and it executes that document — sandboxed, in-process, on every call. The policy is data that travels. The runtime is what runs it. Which means your pricing logic sits on your side of the network, unique to you, and always under your control. As such, it's context-aware and can hold state across a sequence of calls instead of answering one question at a time. Here's what we do with that. ## Monetize: charge in units your customers understand Vendors bill you in tokens, seconds, pages, requests. Your customers don't buy any of those. They buy documents processed, calls handled, deals closed. Sometimes they just want to know the dollar number. Limitr converts between them while usage happens. The rates live in the policy, so a token count becomes credits becomes dollars at the moment of the call, not when the invoice runs. That conversion has to happen in-process, because it needs the raw usage and that customer's rates in the same place at the same time. An outside service can convert numbers you send it afterward. It can't be inside the current pipeline context. This means the invoice is built from the same numbers your product already enforced against. Nothing gets reconstructed at month-end, and the line items make sense to the customer without a footnote. ## Control: one spend cap across an entire pipeline Say an agent makes three calls in a single run — a model provider, a search API, a document parser. Three vendors, three unit systems, three prices. Now put a $2.00 ceiling on the run. A remote service can approve each call on its own. The hard part is holding the running total for that specific run, because the state lives in your process and every check is a round trip to something that doesn't have it. You either track it yourself, which means you wrote the enforcement, or you find out after the run finished. Embedded, the cap is just a number the policy carries as the run goes. The third call gets denied because the first two already spent. And it works across all three vendors because the exchange already put them in the same unit. This enables caps that hold across a whole pipeline, not one call at a time. A faster API doesn't get you there. ## Analyze: know what a run costs, not just what a call costs Per-call margin is relatively easy. Report a cost and a price with every event and anything can add them up. The questions worth asking are shaped differently. What did this run cost? What's our margin on a success versus a failure? Which agent is expensive? Is this customer actually profitable at the rate we gave them? Those need context that only exists while your code is running — where a run started and stopped, which calls belonged to it, whether it worked, which agent made them. A reporting service receives events. It doesn't know what a run is unless you tell it, and once you've built the run boundary and the attribution chain to tell it, you've built the analytics layer and outsourced the addition. Our runtime is already in the pipeline, so it sees the run. And the exchange already put every vendor's usage into one unit, so the run's cost is a single number instead of tokens plus seconds plus pages. This enables us to track cost per outcome instead of just cost per call. ## Why the bar should move None of this is a speed claim. Just being fast doesn't fix it. The limit is that context doesn't survive a round trip — your pipeline state, your call sequence, your costs aren't on the other end of that request unless you sent them, and if you're sending them, you're doing the work the enforcement layer was supposed to do. So we're picky about the word. "At runtime" tells you when a check happens. "A runtime" tells you where your logic lives. The second is a much bigger commitment, and it's the one that decides whether a pricing system can hold state across a run, convert units mid-flight, and tell you your margin before the invoice shows up. When Limitr says runtime, we mean the noun. If you're looking at tools that use the word, ask where the logic actually runs. --- ## How to Control Overhead Cost for AI Products Every LLM call in your AI product — a chat message, a doc summary, an MCP tool call — has a non-deterministic cost attached to it. Let a customer upload 1,000 documents instead of 3, and you're getting an invoice from Big LLM you didn't budget for. The short version of how you get that under control: measure margin per account, per feature, and per vendor — not just cost. Set enforcement limits that guarantee your worst case. Then price so revenue moves with usage instead of trailing behind it. Cost-to-deliver is the metric most teams reach for first, and it's a good start. But it's incomplete, because it's missing revenue. Controlling cost alone only ever caps your downside. The number that actually gives you control is **margin-to-deliver**. > **Margin-to-deliver** is the share of what you charge for a unit of delivered value that you keep after the vendor cost of producing it — `(charged − overhead) / charged` — measured per account, per feature, or per vendor. Optimize margin-to-deliver and you've got two levers to pull: enforcement and pricing. Here's how we think about both, and the order we'd tackle them in. {/* truncate */} :::note As soon as you have usage overhead, usage-based complexity exists in your product, regardless of pricing model and what you present to your customers. This post helps fill the instrumentation gap between token cost-to-deliver and packaging, which should be simplified into customer language and presentation. Packaging itself is not covered in this article. ::: ## Usage Observability An effective control strategy starts with [observation](/analyze). That's why we built an `observe` mode directly into our usage limits — you can't control what you can't see. Vendor dashboards and API key segmentation might get you through the early days, but that approach falls apart the moment you need per-account, per-vendor, per-feature cost and margin analysis. If you don't believe that yet, check back in a few months once your product's grown up a bit. The number you're after is margin-to-deliver. Here's an example: ### Single account, per-feature usage, last 24 hours (2 vendors, 1 pipeline) - Claude Sonnet delivered: 35.67MTok - Google Gemini delivered: 42.5MTok - Outcomes delivered: 2,493 - Charged: $53.78 - Overhead: $42.45 An outcome here is one pipeline run. It may or may not mean anything to the customer — it means something to us. Cost-to-deliver: **$42.45**, or **$0.017** per outcome. Revenue-per-outcome: **$0.0216**, so margin-per-outcome — (charged − overhead) / charged — comes out to **21.1%**. On average, every successful run nets us about 20% margin, for this account. Now we have real numbers to work with. How much lower are we willing to let that margin go? How many more runs before it hits zero, or goes negative? Would a usage limit protect the bottom line, and should every plan or contract get one? ### Attributing raw usage to outcomes The hard part is typically the mapping. A pipeline run generally isn't just one call — it's several, possibly across multiple vendors, and you need to know which outcome each one belonged to before any of the above means anything. In Limitr, an outcome is just another credit, tracked the same way tokens are. Every `allow(...)` call can carry metadata, and it's a common pattern to attribute usage within it, rolling usage up per agent, per feature, via region, the customer's team, or even per run margin and spend data using [spend caps](/spec/concepts#spend-cap). We can do this because Limitr is a [local, context-aware engine](./we-mean-runtime-literally) that can track state over many enforcement checks. Pricing stays accurate with or without any of it, but the metadata is what lets you slice the analytics afterward. It's what makes a line item like "this pipeline consumed 132MTok of Sonnet" possible. :::note[CJ's Tip] Put metrics in terms of real actions or outcomes wherever you can. Keep the raw usage data around for vendor-specific analysis (entitlements should always use the most discrete credit), but the margin-to-deliver KPI itself should be vendor-agnostic — documents read, uploads processed, tool calls made, whatever's meaningful to you. That way, when you switch models, you're comparing against outcomes you (and your customer) care about, not an arbitrary token count that only means something to the vendor selling it to you. Limitr measures real-time margin for every credit and token, and rolls them up (and translates them) automatically — this example is the simplified version. ::: ## Usage Enforcement Once you can see margin-to-deliver per account, feature, and vendor, the first real lever you have to control it is [enforcement](/control) — deciding what a user has access to and how much of it. ### Single account on a "pro" plan @ $200/mo (flat or seats) - AI pipeline @ $0.017 avg cost-to-deliver per outcome - AI data aggregation @ $0.042 avg cost-to-deliver per refinement $200/mo covers 11,764 pipeline runs at that cost. This account used 2,493 in 24 hours — a little over 4 days of runway, nowhere near a 30-day target. Left alone, this account pushes margin negative. The simplest fix, without touching price: cap usage. One option is to limit the pro plan to 392 pipeline runs a day, and cut off data aggregation entirely. Worst-case margin-to-deliver is now 0% — you'll never lose money on this feature or account again. But you may not make any money on it, either. ## Usage Monetization At this point, the case for [usage-based pricing](/monetize) should be clearer. You want revenue to scale with overhead, so margin-to-deliver becomes something you set and optimize through revenue, not just something you defend by cutting cost. There's more than one way to do this — plenty of other [posts](./design-a-flexible-pricing-model-for-ai) cover pricing strategy on its own. But every approach depends on being able to analyze and enforce usage first, which is the whole point of the last two sections. A few of the options, for the example we've been using: - **Credit model** — flexible, supports top-ups, but can be confusing for users when the ROI isn't obvious - **Cost-plus-margin** — clean and accurate, but rigid, invoices that need explaining, hard to upsell, and it tells the customer exactly what your margin is - **Outcome-based** — a middle ground, packaged in the customer's language, but harder to guarantee a positive margin-to-deliver on These aren't mutually exclusive, and the right answer usually differs by feature, by vendor, and by contract. Your objective changes too — margin this month, adoption next. That's the actual argument for keeping the choice in a policy instead of in code: you shouldn't have to ship a release to change your mind. Here's what that policy might look like for our "pro" plan: ### Single account "pro" plan Limitr policy - Monthly subscription: $200/mo (override for annual contracts) - Includes 5 seats, then $29.99/mo per additional seat (hard limit for annual) - Includes 30MTok Claude Sonnet tokens/day, then $5/MTok over (hard limit override for margin control on annual + usage governor for SLA) - Includes 40MTok Google Gemini tokens/day, then $4/MTok over (same overrides) - Includes 500 AI pipeline runs/day, then $0.02/run over (set to observe + unlimited on enterprise + analytics) - Includes 100 AI data aggregations/day, then $0.05/aggregation over (same) The subscription and seats are doing a specific job here: they're the predictable floor that covers your fixed cost regardless of how the account behaves. Everything below them is the part that moves. On the contract and invoice (anything presented to the customer), keep it to subscription, seats, and outcomes when possible — in the customer's language, framed as wins where you can. Average cost per outcome makes a good line item. Put the full cost breakdown further down if needed, in its own section, for whoever wants to dig in. ### Example account usage, 1 month - Claude Sonnet delivered: 1,080MTok — 900MTok included, 180MTok over - Google Gemini delivered: 1,290MTok — 1,200MTok included, 90MTok over - Successful AI pipeline runs: 30,000 — 15,000 included, 15,000 over - Data aggregations: 4,500 — 3,000 included, 1,500 over - Seats: 5, all included Worth noting: that's about 1,000 pipeline runs a day, down from the 2,493 we saw in the first 24-hour window. Usage moves. A plan with a visible included amount and a rate past it changes how an account behaves in a way a flat $200 never does — which is exactly why you want to be watching margin-to-deliver continuously, not modeling it once. #### Invoiced |Line item|Detail|Amount| |---|---|---| |Pro Plan subscription|5 seats included|$200.00| |AI Pipeline Runs|30,000 runs — avg $0.041/run|$1,220.00| |Data Aggregations|4,500 aggregations — avg $0.092/aggregation|$415.00| |**Total due**||**$1,835.00**| #### Usage details **AI Pipeline Runs — $1,220.00** |Component|Amount| |---|---| |15,000 runs over plan @ $0.02/run|$300.00| |Claude Sonnet — 132MTok over @ $5/MTok|$660.00| |Google Gemini — 65MTok over @ $4/MTok|$260.00| **Data Aggregations — $415.00** |Component|Amount| |---|---| |1,500 aggregations over plan @ $0.05 each|$75.00| |Claude Sonnet — 48MTok over @ $5/MTok|$240.00| |Google Gemini — 25MTok over @ $4/MTok|$100.00| Overhead for the month came to $699 — 30,000 runs at $0.017 and 4,500 aggregations at $0.042. Against $1,835 charged, that's a margin-to-deliver of **61.9%**, up from the 21.1% we started with. Nothing got cheaper. Revenue was just allowed to move with the usage driving the cost. The included usage gives you a good baseline margin-to-deliver, depending entirely on any base platform fees. The cost vs price per credit changes the margin-to-deliver for every credit beyond included limits. And separating internal/external outcomes vs vendors vs included provides flexibility per account, feature, and vendor to move margin-to-deliver according to your goals. :::note[CJ's Tip] A credit model could be used with this exact policy to provide even more flexibility for both the provider and consumer. Included, committed, and/or top-ups of abstract credits can be applied at different exchange rates across all other discrete credits, like tokens. For your user, this actually simplifies what they're looking at, because it's one common unit, clearly scaled to make it easier for them to track, budget, allocate, and commit on. Could also allow them to place their own usage caps in a single unit, so that they never go over what they expect, regardless of how they're used. ::: ## Two levers, one usage policy Cost-to-deliver tells you what an account costs. Margin-to-deliver tells you whether it's worth having, and gives you the numbers you need to actually stay in control. **Enforcement sets your baseline.** A limit is a guardrail around the worst case. Cap pipeline runs at 392/day and you've decided, in advance, that this account cannot lose you money. It holds whether anyone's watching or not. **Pricing moves that baseline.** Charge for overage and revenue rises with cost instead of being eaten by it. That's the 21.1% → 61.9% swing above. Enforcement without pricing protects your margin by refusing actions — a 0% floor and a hard cap, for example. For some situations, like agent tool calls or the time an agent gets to converge on a turn, this layer of control makes sense regardless of pricing. Pricing without enforcement can scale revenue with cost, right up until one run consumes 300x more AI overhead than accounted for. Both require per-account, per-feature, and per-vendor observability. You can't set a limit you can't measure against, and you can't price an outcome you can't count. Which is where this usually falls apart. Analytics sit with one vendor, limits sit in application code, prices sit in the billing system — and the three drift. The number you analyzed isn't the number you enforced on, and neither one is the number you invoiced. This is what we built the Limitr policy for. One document defines the credits, what they cost you, what you charge for them, what each plan includes, and what happens at the limit — observe, soft, or hard. That same document is what executes at the moment of the call. So the margin you're analyzing, the limit you're enforcing, and the line item you're invoicing all come out of one place, in real time, and they can't drift apart. To see it in action, check out the [live monetization demo](/spec/monetize/demo) that lets you play with a simple policy, tracking actual usage right here in your browser.