Give an allowance, charge for going over

The pricing shape where 1,000 are included and the 1,001st still works and costs two cents. How to count it, how to show it honestly, and how it becomes a line on a Stripe invoice.

Free core 14 min Intermediate

The product here is Inkwell, an AI writing assistant. $29 a month includes 1,000 generations. The 1,001st still works. It costs two cents and lands on the next invoice. Nobody is ever stopped mid sentence.

That is the metered feature kind. It is the right shape when saying no costs you more than the work costs, and when your customers would rather be billed than interrupted.

#1. The plan

defmodule Inkwell.Plans do
  use AuroraMeter.Plans

  plan :free do
    price 0
    limit :generations, 25, :hard        # free really does stop
  end

  plan :writer do
    price 2_900                          # $29.00
    metered :generations, included: 1_000, unit_price: 2   # 2 cents each after 1,000
  end

  plan :studio do
    price 9_900
    metered :generations, included: 5_000, unit_price: 1   # cheaper per unit
  end
end

included and unit_price are both whole numbers, and unit_price is in cents. unit_price: 2 means two cents.

#What those two numbers are actually for

This surprises people, so here it is bluntly:

included and unit_price are for your estimates and your screens. When you bill through Stripe, Stripe’s price tiers decide what the customer is charged.

They let you draw “you are 240 over, about $4.80” without calling Stripe. They do not set the price. Change unit_price here and not in Stripe and your dashboard lies while the invoice stays right. Keep them in step deliberately; Get overage onto the invoice covers the Stripe side.

#2. Counting

AuroraMeter.track(org, :generations)

That is the whole hot path: an in-memory counter increment with no database call, so you can put it anywhere, including inside a loop.

A background worker writes totals to Postgres every five seconds. That trade is why it is fast, and it has a consequence worth deciding about on purpose: if the machine dies, up to five seconds of counts die with it.

For a two cent generation, losing a few is cheaper than the machinery to never lose one. If that is not true for you, say so per feature:

config :aurora_meter, durable_features: [:generations]

Durable features write straight through to the database on every call. Correct, slower, a database write on every request. Choose per feature, not globally.

#Counting more than one

A single request that produced five variants is five units:

AuroraMeter.track(org, :generations, 5)

And a correction, if you overcounted, is a negative number:

AuroraMeter.track(org, :generations, -1)

#3. Gating, or rather not gating

AuroraMeter.check(org, :generations)
# :ok, always, on :writer and :studio

A metered feature never refuses. That is the point of it.

If you find yourself wanting it to refuse at some ceiling, you either want limit ... :hard, or you want both: a metered feature for the money, and your own sanity check for abuse.

defmodule Inkwell.Generation do
  @abuse_ceiling 50_000

  def run(org, prompt) do
    if AuroraMeter.usage(org, :generations) > @abuse_ceiling do
      {:error, :contact_support}
    else
      AuroraMeter.with_quota(org, :generations, fn -> Inkwell.AI.generate(prompt) end)
    end
  end
end

with_quota/4 on a metered feature still counts, still gives the count back if your function crashes, and simply never refuses. Using it rather than bare track/3 means you get the crash safety for free, and this code reads exactly like your gated features do.

#4. What the customer sees

AuroraMeter.quota(org, :generations)
# %{feature: :generations, kind: :metered, used: 1_240, included: 1_000,
#   overage: 240, unit_price: 2, limit: nil, remaining: :unlimited,
#   percent: 100, enabled: true, period: %{...}}

percent is clamped to 100 and will never tell you they went over. At 1,240 of 1,000 it reads 100, and it reads 100 at 10,000 too. That is fine for drawing a bar and useless for writing a sentence.

So read overage, which is 240 here, and put the number next to the bar. A customer 240 past their allowance should not see the same screen as one sitting exactly on it.

<div class="quota">
  <p><%= @q.used %> of <%= @q.included %> generations</p>

  <p :if={@q.overage > 0}>
    <%= @q.overage %> over, about
    <%= AuroraMeter.Credits.Money.format(@q.overage * @q.unit_price * 10_000) %>
    on your next invoice
  </p>
</div>

That * 10_000 turns cents into micro-dollars, which is what Money.format/2 takes: 240 units at 2 cents is 480 cents, which is 4,800,000 micro-dollars, which formats as “$4.80”.

#5. Charting it

AuroraMeter.history(org, :generations, days: 30)
# [%{date: ~D[2026-02-10], value: 41}, %{date: ~D[2026-02-11], value: 0}, ...]

Daily buckets, already filled in and sorted oldest first, so a quiet Sunday shows as a zero rather than a hole in the chart.

History is on by default. Turn it off with config :aurora_meter, history: false and history/3 returns an empty list rather than raising.

#6. Warning them before the invoice does

Nobody enjoys discovering an overage after it has been charged. Pro fires alerts as a customer approaches and crosses their allowance:

config :aurora_meter_pro, alert_handler: &Inkwell.Billing.quota_alert/1
defmodule Inkwell.Billing do
  def quota_alert(%{tenant_key: key, feature: feature, percent: percent}) do
    org = Inkwell.Orgs.get_by_key!(key)

    cond do
      percent >= 100 -> Inkwell.Mailer.overage_started(org, feature)
      percent >= 80 -> Inkwell.Mailer.approaching_allowance(org, feature, percent)
      true -> :ok
    end
  end
end

Alerts are deduplicated per customer, per feature, per period, so somebody sitting at 81% for a fortnight is emailed once and not every ten minutes. If your handler fails, the “already sent” record is rolled back and the next sweep tries again, so an alert that could not be delivered is not silently marked delivered.

The thresholds are yours:

config :aurora_meter_pro, alert_thresholds: [50, 80, 100]

#7. Getting it onto the invoice

The core counts. It does not charge. Turning 240 units of overage into money needs Pro, which reports usage to a Stripe Billing Meter on a schedule:

config :aurora_meter_pro,
  stripe_prices: %{writer: "price_flat_writer", studio: "price_flat_studio"},
  stripe_metered_prices: %{writer: ["price_writer_overage"]},
  stripe_meters: %{generations: "generations"}
config :inkwell, Oban,
  queues: [aurora_meter: 5],
  plugins: [{Oban.Plugins.Cron, crontab: [
    {"*/5 * * * *", AuroraMeter.Pro.UsageReporter}
  ]}]

In Stripe, give the metered price a graduated tier: the first 1,000 at $0, everything above at two cents. The reporter sends the difference since the last successful report, and Stripe applies the tiers. That is why included lives in two places, and why the two have to agree.

The full Stripe side, including what happens when a send times out and nobody knows whether it arrived, is in Get overage onto the invoice.

#8. Free tiers should actually stop

Notice that Inkwell’s free plan uses limit ... :hard, not metered. That is deliberate and worth copying.

A metered free plan bills a customer who never gave you a card. You cannot collect, and they get the product for nothing. Free tiers should hit a wall. Paid tiers should bill.

The move between the two is one call:

AuroraMeter.subscribe(org, :writer)

The same feature name, :generations, is a hard cap on one plan and a metered allowance on the next. The counter carries across untouched; you are only changing the rule read against it.

#9. Choosing between the two money shapes

Metered overage is one of two shapes. The other is prepaid credit, where customers buy money up front and spend it per request.

Metered overage Prepaid credit
When you are paid after use, on the invoice before use
Who can run up a bill anybody with a card on file nobody
Feature kind metered counter
Fits monthly subscriptions with a generous allowance pay as you go APIs, AI tokens
Risk an unpaid invoice none, but customers can run dry mid job

Nothing stops you doing both: a monthly plan with an allowance, plus credit for the occasional big job. They are separate systems and they do not interfere.

Sell credit up front, spend it per request is the other half.

#The whole thing, end to end

# a free user hits the wall at 25
AuroraMeter.with_quota(org, :generations, fn -> Inkwell.AI.generate(p) end)
# => {:error, :limit_exceeded}

# they subscribe
AuroraMeter.subscribe(org, :writer)

# the same call, now allowed, and it stays allowed past 1,000
AuroraMeter.with_quota(org, :generations, fn -> Inkwell.AI.generate(p) end)
# => {:ok, "..."}

# the dashboard is honest about the money
AuroraMeter.quota(org, :generations)
# %{used: 1_240, included: 1_000, overage: 240, unit_price: 2, ...}

# every five minutes, Pro tells Stripe about the difference, exactly once

Keep going

The core is free. Pro is the Stripe half.

Everything the free MIT package does is yours with no account and no expiry. Aurora Meter Pro adds Stripe checkout, metered usage reporting, prepaid top-ups, dashboards and alerts, and every plan starts with a free trial.