Reading about a metering library and running one are different experiences. The sample is a whole Phoenix application, MIT licensed, that meters a fake AI product: it holds credit before the work, records the tokens as durable facts, settles the hold for what the work actually cost, and shows you the ledger.
It runs with Elixir and a Postgres server. No API key, no Stripe account, no network call of any kind.
#Getting it running
It lives inside the aurora_meter repository at
examples/aurora_meter_example_ai.
cd examples/aurora_meter_example_ai
mix setup
mix sample.seed
PORT=4021 mix phx.server
mix setup fetches dependencies, creates and migrates the database and builds
assets. mix sample.seed creates two organisations, four users and four credit
lots, and prints the accounts and the shared password. Log in at
/users/log-in.
The database defaults to localhost:5490, which is the Postgres the package’s
own test suite uses. Override DB_HOST and DB_PORT if yours is elsewhere, and
mix ecto.reset starts again from nothing.
Five pages are worth visiting in order: /generate, /history, /ops,
/dev/tools, and the POST /api/generate endpoint behind the plug.
#The work is deterministic, and it is obviously not an AI
AuroraMeterExampleAi.Tokens computes a token count from the prompt and then
assembles a paragraph out of the words of the prompt itself. It does that
deliberately, so that nothing it produces could be mistaken for a model’s
answer.
The generator is seeded from a hash of the prompt and the model, and it threads its own state rather than touching the calling process’s, so the same prompt and model give you the same token count and the same bytes on every machine, and running the sample does not make your own tests non deterministic.
Two models, nimbus-1 and nimbus-1-mini, at ten millionths of a dollar per
token, so ten thousand tokens is ten cents.
A prompt starting fail: raises, which is how the failure recipes reach into
the middle of the work.
#One string, three jobs
The line worth reading twice is in AuroraMeterExampleAi.Generations.create/3,
and it is the pattern the whole library is designed around:
AuroraMeter.with_quota(org, :images, 1, fn ->
AuroraMeter.Credits.with_credits(org, estimate, "gen:" <> request_id, fn ->
result = Tokens.run(prompt, model)
AuroraMeter.record(org, :tokens, result.total, id: "gen:" <> request_id, occurred_at: at)
{:ok, result, result.total * 10}
end)
end)
request_id is the primary key of the application’s own row, the reference of
the credit hold, and the identity of the durable event. When somebody asks what
happened to one generation, one string answers in three tables. The callback’s
third element is the real cost, so the hold settles for what the work actually
took rather than for the estimate.
#Eight failure recipes
This is the part of the sample worth the download. Each recipe reaches into the middle of a real flow, breaks one thing, and prints what the application, the database and the ledger each believe afterwards.
mix sample.failure # list them
mix sample.failure untrappable_death # run one
mix sample.failure all # the seven that need no Pro key
mix sample.failure --check # re-assert the last run against its saved report
| Recipe | What it shows |
|---|---|
insufficient_balance |
a refusal before any work, and the two different reasons a wallet can refuse |
callback_raise |
a raise inside the work, and the hold and the reservation coming back |
untrappable_death |
a process killed between the durable event and the application’s own row |
worker_retry |
two delivery attempts and one effect |
duplicate_event |
one identity recorded twice: one event, one delta, one intent |
exporter_timeout |
an outcome nobody knows, which is never retried automatically |
late_correction |
a correction after the period, bounded and immutable |
customer_cancellation |
a cancelled subscription and a refunded top up, reversed once (needs Pro) |
untrappable_death is the one to run first. It is the failure people assume a
library handles and then discover it does not, and the report shows you exactly
which of the three records moved and which did not.
Every report has the same seven sections: setup, trigger, application, database,
ledger, recovery, uncertain. That last section is the important one. It is where
a recipe says what nobody can know from inside the system, which is the honest
answer for exporter_timeout and the reason that state needs a person.
No recovery step edits a row. The sample’s own test suite fails if the
failure documentation ever grows an UPDATE, an INSERT or a DELETE, and
cross checks the document’s headings against the implemented recipes in both
directions, so a recipe cannot be documented and missing, or implemented and
undocumented.
A Pro recipe run without the Pro profile aborts by name. It does not degrade into a simulation, because a simulated proof is worse than no proof.
#Turning the Pro half on
Optional, and it needs a Pro key.
mix hex.organization auth phxtemplates --key "$AURORA_HEX_READ_KEY"
cp .env.example .env
export AURORA_SAMPLE_PRO=1
mix deps.get
mix ecto.migrate
mix ecto.migrate --migrations-path priv/repo/pro_migrations
PORT=4021 mix phx.server
The flag switches lockfile and build directory as well as configuration, because
two dependency resolutions need two of each. It adds Oban, the Pro supervision
tree, a /billing page, the Stripe webhook mounted in the endpoint above
Plug.Parsers, and an outbox that calls both halves.
The guard that makes this trustworthy is test/pro_absent_test.exs: it reads
every compiled module’s import table and fails if anything outside the two
guarded trees references AuroraMeter.Pro. The core profile is not “Pro with
the calls commented out”; it is an application that could not call Pro if it
tried.
#What to take from it
Three things, and none of them is the code.
The first is the shape of with_quota wrapping with_credits wrapping the
work, and one identity threaded through all three.
The second is that every interesting failure leaves the system in a state you can describe, and the recipes describe them rather than asserting that the system recovered.
The third is the uncertain section. A system that can tell you what it does
not know is one you can operate.
#What to read next
- Build a metered SaaS, end to end is the same shape, built from nothing, in your own application.
-
Durable events and corrections
explains the
recordcall the sample makes.
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.