Subscription tools manage renewals. AI products need metering, credits and quotas enforced per request. Here is where the split happens and how to build it.
A customer signs up on the 1st, pays a fixed amount, and renews on the 1st of the next month. That is the model almost every subscription tool was designed around, and it works fine until the thing you are selling is inference.
AI workloads do not consume evenly. One user runs four prompts a week. Another wires your endpoint into a batch job and burns more tokens in an afternoon than the first user will all quarter. Both are on the same plan. The renewal date is the least interesting fact about either of them.
What is the difference between subscription management and usage billing?
Subscription management answers "is this customer still paying?" Usage billing answers "what did this customer consume, what is it worth, and are they still allowed to consume more?"
The second question has to be answered while the request is in flight. That is the part renewal tooling was never built to do, and it is why teams shipping AI products end up running two systems: one that handles the card on file, and one that handles everything the product actually meters.
The three things AI billing needs that renewals do not
Metering. Every billable action becomes an event with a customer attached. Not a nightly rollup of API logs, not a counter in Postgres that someone increments in application code. An event stream you can query, audit, and replay when a customer disputes a line item.
Credits. Prepaid balances, trial grants, promotional top-ups, and expiry rules. Credits are a different primitive from a subscription. A customer can hold a balance without holding a plan, and can hold both at once with a defined order of deduction.
Entitlements. What a plan allows, checked before an expensive operation runs. Model access, concurrency ceilings, monthly caps. If this check lives in your application code as a series of if-statements against a plan name, every pricing change becomes a deploy.
Miss any of these and the gap gets filled with glue code that finance cannot read and engineering does not want to own.
How the tooling landscape splits
Most of the confusion in this category comes from tools solving genuinely different problems while using overlapping words. It is worth being precise about scope.
| Tool | What it is scoped to |
|---|---|
| Paddle | Payment and merchant-of-record layer. Handles checkout, tax, and currency across regions. Pairs with a metering system rather than replacing one. |
| Lemon Squeezy | Recurring billing and checkout for small teams. Metered add-ons exist through API extensions. Good for validating a price before a metering stack exists. |
| SubscriptionFlow | Subscription lifecycle automation. Renewals, trials, upgrade workflows driven by events. |
| Orb | Strong fit for straightforward self-serve usage pricing. The ceiling shows up as pricing and go-to-market motions get more complex, which is the point where teams look for an enterprise-ready billing platform instead. |
| Metronome | A metering point solution aimed at engineers. Scope stops at usage measurement, so billing, invoicing and pricing iteration live in other systems. |
Flexprice sits in the last row's category but with a wider scope. Metronome is a metering engine for developers. Flexprice is end-to-end billing and pricing infrastructure: metering, billing, invoicing, reporting and pricing experimentation, all built on a raw event data model.
Flexprice is enterprise-grade, open source usage based billing infrastructure for AI and SaaS companies. It can be deployed in your own VPC, on-prem, or on Flexprice's managed cloud.
What the request path looks like
The mechanism is simpler than the category makes it sound. Your product emits an event when something billable happens. The billing layer aggregates it, prices it, decrements a wallet if one applies, and exposes the result.
A billable event carries the feature name and your own customer identifier:
{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": {
"credits": 2,
"model": "gpt-4",
"region": "us-east-1"
},
"event_id": "evt_abc123",
"timestamp": "2025-08-22T07:05:49.441Z",
"source": "api"
}
event_name has to match the metered feature you configured. properties carries whatever the aggregation reads, so a Sum aggregation on credits adds that field across the window. The event_id is yours, which is what makes a retry safe instead of a double charge.
Sending it is one POST:
curl --request POST \
--url https://api.cloud.flexprice.io/v1/events \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your_api_key>' \
--data '{
"event_name": "model.usage",
"external_customer_id": "cust_123",
"properties": { "credits": 2 }
}'
The response confirms the event was accepted for processing rather than blocking on the full pricing path:
{
"event_id": "event_01K389J4M1F1NZG6XP0AMD6J52",
"message": "Event accepted for processing"
}
From an inference service, that becomes a call you make after the work completes, on the same code path that already knows the token count:
type Event struct {
EventName string `json:"event_name"`
ExternalCustomerID string `json:"external_customer_id"`
Properties map[string]interface{} `json:"properties,omitempty"`
Source string `json:"source,omitempty"`
}
func sendEvent() error {
event := Event{
EventName: "model.usage",
ExternalCustomerID: "cust-test-customer",
Properties: map[string]interface{}{
"credits": 2,
},
Source: "api",
}
jsonData, err := json.Marshal(event)
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://api.cloud.flexprice.io/v1/events", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", os.Getenv("FLEXPRICE_API_KEY"))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Printf("Event transmitted with status: %d\n", resp.StatusCode)
return nil
}
High-volume services should batch rather than send one request per inference. The bulk endpoint takes an array of the same objects. The full field reference lives alongside it in the event ingestion docs.
Where does the plan logic live?
Outside your services, if you want to change pricing without a deploy.
The pattern that holds up is a clean split. Your application knows what happened. The billing layer knows what it costs and what the customer is allowed to do. Those are separate concerns and they change on completely different schedules. Product ships a feature every week. Pricing changes once a quarter, but when it changes, it changes across every tier at once.
Concretely, that means a metered feature defines the unit and the aggregation, a plan attaches prices to it, and an entitlement decides access. Creating a Metered Feature covers the first, and entitlement grants cover the access side.
Credits are worth calling out separately because teams reach for them late and then have to retrofit. A wallet holds a balance that is independent of the subscription. Trials become metered grants with a hard cap instead of a fake demo mode, and a prepaid enterprise commitment becomes a balance with an expiry rather than a spreadsheet. Prepaid and promotional credits covers how the balance types differ.
A concrete flow
An inference API with plan-level token limits and a credit balance ends up with something like this:
- Request arrives. The service checks the customer's entitlement and balance before running the model.
- The model runs. The service emits a usage event with the token count in
properties. - The billing layer aggregates the event, applies the plan's rate, and decrements the wallet.
- A webhook fires when the balance crosses a threshold, so the customer hears about it before the invoice does.
- At cycle close, usage, credits and any recurring fee resolve into one invoice.
None of those steps require billing tables in your product database. The step most teams skip is the fourth, and it is the one that prevents the support ticket.
What this is not
This layer does not replace your payment processor. It decides what a customer owes. Something else moves the money, and the two should stay separable so that a change to one does not force a change to the other.
It also does not remove the hard part of pricing, which is choosing a unit customers can reason about. Metering any unit is a solved problem. Picking one that maps to value is a product decision, and no infrastructure recovers from getting it wrong.
Getting started
Flexprice is open source. You can read the ingestion path, run it against a test feature, and see what the aggregation does to your own event shape before committing to anything: docs.flexprice.io.
Top comments (0)