What users spent on the assistant (spec §7.3), counted from the AI request
journal (phoenix_kit_ai_requests) rather than from the module's own
messages: a row of the user (user_uuid) whose metadata.source is
"agent" — every model call of the module passes both, so a call that
describes an attachment is counted too. The journal is not pruned by age, so
the month never loses rows. The journal keeps a cost in dollars (cost, a
numeric that can be finer than a micro-dollar); the sums are in
micro-dollars ($1 = 1_000_000): each window is added up in dollars and only
then rounded, half up, to a whole micro-dollar
(PhoenixKitAgent.Config.cost_to_micro/1) — two rows of $0.0000004 are
1, not 0. Only endpoints that report a cost add to it.
Every row with a cost counts, whatever its status: a call that failed or
timed out after the provider billed it is money spent all the same. Today
phoenix_kit_ai writes a cost only on its "success" rows (a failed call is
logged with cost NULL, which adds nothing), so the sums equal a
success-only count. For the same reason the queries do not filter on
status = 'success' and cannot use core's partial index
phoenix_kit_ai_requests_user_spend_idx; they go by the journal's
user_uuid index and the month's window.
The windows are calendar days and months in the site time zone
(Settings → time_zone; PhoenixKit.Utils.TimeZone), each from its local
midnight to the next one — so a day that has 23 or 25 hours is counted right.
Turns are counted from the module's own messages (turns_today/2): a turn
is one user message with a turn_ref.
Summary
Functions
What each of user_uuids spent today and this month (the windows of now):
%{user_uuid => %{day_micro_usd:, month_micro_usd:}}, every user in the
answer, zero when there is nothing.
Micro-dollars as dollars with at least two decimals: 1_500_000 is "1.50".
The local day and month that contain now, as keys: %{day: "2026-10-19", month: "2026-10"}.
How many turns each of user_uuids started today (the local day of now),
in any of their chats: %{user_uuid => count}, every user in the answer.
Everyone who used the assistant this month (the local month of now): a
request in the journal or a turn. User uuids; a journal row whose user was
deleted has none and is left out, and so is a call the AI guard refused
(metadata.refused): it was never made.
The local day and month that contain now, as UTC windows.
Types
@type spent() :: %{ day_micro_usd: non_neg_integer(), month_micro_usd: non_neg_integer() }
@type window() :: {DateTime.t(), DateTime.t()}
Half-open: from the first instant, up to but not including the second.
Functions
@spec for_users([String.t()], DateTime.t()) :: %{required(String.t()) => spent()}
What each of user_uuids spent today and this month (the windows of now):
%{user_uuid => %{day_micro_usd:, month_micro_usd:}}, every user in the
answer, zero when there is nothing.
Micro-dollars as dollars with at least two decimals: 1_500_000 is "1.50".
@spec period_keys(DateTime.t()) :: %{day: String.t(), month: String.t()}
The local day and month that contain now, as keys: %{day: "2026-10-19", month: "2026-10"}.
@spec turns_today([String.t()], DateTime.t()) :: %{ required(String.t()) => non_neg_integer() }
How many turns each of user_uuids started today (the local day of now),
in any of their chats: %{user_uuid => count}, every user in the answer.
@spec users_this_month(DateTime.t()) :: [String.t()]
Everyone who used the assistant this month (the local month of now): a
request in the journal or a turn. User uuids; a journal row whose user was
deleted has none and is left out, and so is a call the AI guard refused
(metadata.refused): it was never made.
@spec windows(DateTime.t()) :: %{day: window(), month: window()}
The local day and month that contain now, as UTC windows.