How money is written in Tempio
Every amount is an integer number of minor units with its ISO-4217 currency beside it, never a float and never a bare number. This note is the denominations, what each platform sends on the wire, and the rule that money never crosses currencies without saying so.
Last reviewed 2026-09-13.
An amount in Tempio is an integer number of the account's minor units, with the account's three-letter ISO-4217 code stored beside it. Never a float, never a fraction of a cent, never a number whose denomination has to be inferred from context. A number without its currency is not money here, and an agent that quotes one has quoted a digit count.
The denominations
How many minor units make a major one is the currency's business, not a
universal hundred. lib/ads/money.ts is the one place that knows:
- Exponent 0 — the major unit is the minor unit. JPY, KRW, VND, CLP, ISK,
PYG, UGX, RWF, GNF, KMF, DJF, XOF, XAF, XPF, BIF. ¥1,500 is
1500. - Exponent 2 — the majority. $15.00 is
1500. - Exponent 3 — thousandths. KWD, BHD, OMR, TND, JOD, IQD, LYD. KD 1.234 is
1234.
A code the table has never heard of answers 2 rather than throwing: a platform can hand us a currency nobody anticipated, and two decimals is both the ISO majority and the least damaging way to be wrong. MGA and MRU look like base-five oddities and are not on the zero-decimal list, because ISO 4217 assigns them exponent 2 and the default already answers correctly.
Hard-coding ×100 anywhere is a bug that shows up on the first non-two-decimal
account and not before. There are two conversion helpers and no third:
majorToMinor("12.50", currency) for a typed form value, which answers null on
empty, negative or non-numeric input rather than zero; and
majorToMinorValue(value, factor) for a platform amount, where Math.round is
load-bearing rather than cosmetic — 19.99 * 100 is 1998.9999999999998 in
IEEE-754, and truncation would quietly bill a cent short on every second
budget.
What each platform sends
- Google states money in micros: one millionth of a MAJOR unit. A micros figure is not a minor-unit figure and the two differ by more than a factor of ten on a zero-decimal account.
- Meta states money in subunits under its own currency offset table,
which does not always agree with the ISO exponent, so the adapter converts by
the difference between the two rather than assuming they match. Where Meta's
published table does not name a code, the adapter falls back to ISO and
attaches
meta_currency_offset_unverifiedrather than refusing: the denomination is unconfirmed, so the figure's scale is unconfirmed too, and the warning is what says so. - TikTok sends entity budgets as a JSON float and reporting money as a decimal string. Both go through the same rounding helper for the reason above.
One currency per scope
An ad account bills in exactly one currency, and that code is the authority for
every amount that belongs to it. A draft payload's currency is derived
from the owning account on the way in; a client that supplied one has it
overwritten without comment, because the account bills in what it bills in and
a payload that disagreed would be a lie the apply gate refuses later. If an
account's currency changes after a draft was written, the draft carries
stamped_currency_changed and should be written again rather than approved.
A media plan is in exactly one currency too. Its items state a whole-flight total in the plan's minor units, and materializing the plan refuses any item whose resolved ad account bills in a different code — money never silently crosses currencies, so the fix is to point the item at an account in the plan's currency, or to plan that window in the account's currency.
Never convert without saying so
Conversion exists in exactly one place: a read that spans several currencies converts through published rates into a labelled estimate, carrying the date of the oldest rate it used. That label is the whole permission. Nothing else converts — no guardrail, no budget, no cap, no draft. A cap is compared against an amount in its own currency or it is not compared at all, and a sum across currencies with no published rate is returned in raw order with the fact attached rather than dressed up as a ranking.
When you report an amount, report the code with it, and say "estimate" when the result says estimate.
Sources
- minorUnitExponent, minorUnitFactor, majorToMinor, majorToMinorValue —
repo://apps/app/lib/ads/money.ts(read 2026-09-13) - formatMinor — one rendering of an amount and its currency —
repo://apps/app/lib/ads/format.ts(read 2026-09-13) - One currency per plan; the account-currency match at materialize —
repo://apps/app/lib/ads/plans.service.ts(read 2026-09-13) - Currency is stamped from the ad account, never accepted from a caller —
repo://apps/app/lib/ads/drafts.service.ts(read 2026-09-13) - Metrics — the Money section of the product page —
repo://apps/docs/content/docs/metrics/index.mdx(read 2026-09-13)