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:
includedandunit_priceare 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: %{...}}
percentis 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.