Does the invoice total actually add up? A proposed axis for money arithmetic in AI-generated apps (September 2026)
A proposed BuilderProof axis measuring whether the applications AI app builders generate store and compute money exactly, or only render it that way. Seven weighted signals, four postures, a ten-step protocol with a control step. Pre-registered: no builder is scored here.
Updated on September 7, 2026
On this page
Quick answer. Nobody has measured whether the applications AI app builders generate compute money correctly, and this page does not measure it either. What follows is a pre-registration: a proposed axis, the weights we would use, the failing case for each signal, and the exact protocol we would run. No builder is scored here and no builder is named in a posture. The reason to publish the rubric before the results is that a rubric written after you have seen the numbers is a rubric you can no longer defend.
The short version of the finding that motivated it: the two databases and the one language these builders overwhelmingly emit all document, in their own reference material, that the obvious way to hold a price is the wrong way to hold a price. The guidance is old, settled, and written by the people who maintain the runtime. What nobody has checked is whether the generated code follows it.
Why this is not already covered
Three axes in this series come close enough that the boundary has to be drawn explicitly, in both directions, or the axis is not worth proposing.
Against type-safety posture
Our type-safety posture axis scores whether the emitted code declares types and survives its own compiler on strict settings. It is the nearest neighbour, and it is also the cleanest boundary in the series, because the neighbour's own governing document says it cannot see this.
The TypeScript handbook, verbatim: "JavaScript does not have a special runtime value for integers, so there's no equivalent to int or float - everything is simply number."
A type checker can therefore confirm that an amount is a number and has no way to ask which kind of number it is. That is not a defect in the type-safety axis; it is the boundary. The two scores move in opposite directions and both directions are real. A build can annotate every monetary field as number, pass tsc --noEmit under strict with zero suppressions, score at the top of the type-safety axis, and be maximally wrong here, because the type it declared so carefully is the inexact one. A build can hold amounts as strings, pass them through an arbitrary-precision library, score badly on typed boundaries because those strings are weakly modelled, and be exactly right here.
Against input validation and data integrity
Our input-validation and data-integrity axis includes a semantic and range-checks signal covering bounds and business ranges, which touches prices. The boundary is not the field, it is the moment.
Validation asks whether the value that arrives is acceptable. This axis asks what happens to it afterwards, during arithmetic, once it has been correctly accepted. A validator that receives 19.99, confirms it is a positive two-decimal number inside the allowed range and admits it, has done its job perfectly. The defect happens three lines later, when that accepted value is multiplied by a quantity. Running the other way, an application with no boundary validation at all can still produce exact totals, if it stores minor units as integers and never performs binary fractional arithmetic. Neither score predicts the other.
Against database migration and schema-change safety
Our database migration and schema-change safety axis scores whether a change to the data model preserves the rows already in the table. A column-type change is exactly where a monetary storage decision gets made, or broken, so the surfaces genuinely touch. The boundary is that the migration axis scores the safety of the transition and this one scores the correctness of the resting state. A builder can execute a flawless, reversible, fully backfilled migration that converts a monetary column to double precision, scoring well there and badly here. A builder can pick the right column type at generation time and have no migration story at all.
Three documented facts that make this measurable
1. The database tells you not to use the type most code uses
PostgreSQL's numeric types reference classifies real and double precision as inexact and states the consequence plainly.
PostgreSQL, verbatim: "Inexact means that some values cannot be converted exactly to the internal format and are stored as approximations, so that storing and retrieving a value might show slight discrepancies."
The same page gives the instruction directly: "If you require exact storage and calculations (such as for monetary amounts), use the numeric type instead." And of numeric it says it is "especially recommended for storing monetary amounts and other quantities where exactness is required."
This is not obscure. It is the second and third subsections of the first data-type page in the manual.
2. The same rounding call returns different answers depending on the column type
The strongest evidence that storage type is an arithmetic decision rather than a storage preference is that PostgreSQL documents the divergence itself, with a runnable example. Rounding a value ending in exactly one half gives one answer for numeric and a different answer for double precision.
PostgreSQL, verbatim: "When rounding values, the numeric type rounds ties away from zero, while (on most machines) the real and double precision types round ties to the nearest even number."
The manual then prints the table. At -2.5 the numeric result is -3 and the double-precision result is -2. At 0.5 the numeric result is 1 and the double-precision result is 0. At 2.5 the numeric result is 3 and the double-precision result is 2. Same function, same input, two answers, and the only difference is the type the value was resting in. An application that rounds a half-cent is picking a rounding policy whether or not anyone wrote one down.
3. The language has one number, and it is the inexact one
On the application side there is no choice to get wrong, only a consequence to handle.
MDN, verbatim: "The JavaScript Number type is a double-precision 64-bit binary format IEEE 754 value, like double in Java or C#."
MDN also documents the integer boundary: values are representable "without loss of precision in the range -2^53 + 1 to 2^53 - 1", because the mantissa holds 53 bits. That range is enormous for cents and irrelevant for fractions, which is the point. The fractional half is where money breaks, and a builder that fetches a numeric column into a JavaScript number has undone the database's exactness before the first addition.
Note also what toFixed() is. MDN describes it as returning "a string representing the number in fixed-point notation." It is a formatter. It produces a correct-looking string from an incorrect value and stores nothing.
A note on currency scale
Storing integers is the standard remedy, and it has a second-order trap that is entirely checkable. The scale is a property of the currency, not a constant.
Stripe's supported currencies reference, verbatim: "Currencies are two-decimal currencies unless otherwise specified. All API requests expect amount values in the currency's minor unit."
The documentation gives the two cases side by side: 1000 to charge 10 USD, and 10 to charge 10 JPY. Stripe lists sixteen zero-decimal currencies with general API support, including JPY, KRW, VND, CLP and the CFA franc codes. A generated checkout that hardcodes a multiplication by one hundred is correct for most of the list and wrong by a factor of one hundred for the rest. Stripe even documents a currency that is neither, noting that ISK "transitioned to a zero-decimal currency, but backward compatibility requires you to represent it as a two-decimal value, where the decimal amount is always 00."
The proposed rubric
Seven signals. Weights are a starting point for community revision, not a settled formula.
Scroll to see more
| Signal | Weight | What a failing case looks like |
|---|---|---|
| Monetary storage type | 22 | Prices, totals and balances rest in a floating-point column, or in a JSON blob with no declared scale, rather than an exact type or an integer minor unit |
| Arithmetic exactness end to end | 20 | An exact column is read into a binary floating-point value, summed or multiplied there, and written back, so the exact type protects nothing |
| Rounding policy declared and applied once | 16 | No rounding rule exists anywhere; halves resolve differently in different code paths, and the result depends on which layer happened to round first |
| Currency scale correctness | 14 | A fixed multiplication by one hundred is applied to every currency, so zero-decimal currencies are charged a hundred times the intended amount |
| Payment-provider boundary agreement | 12 | The amount sent to the provider is derived independently of the amount stored, so the two can disagree without anything failing |
| Aggregate and reconciliation behaviour | 9 | Summing a column of many rows drifts from the sum of the displayed line items, and no reconciliation check would catch it |
| Display formatting separated from stored value | 7 | The formatter is the only thing enforcing two decimals, so the interface is always correct and the persisted value is not |
Three weighting decisions worth arguing about now
Why storage type carries the most. It is upstream of every other row. A wrong column type cannot be repaired by careful arithmetic later, because the approximation is already stored. Every other signal describes damage done to a value in flight; this one describes damage done to the value at rest.
Why arithmetic exactness is almost as heavy. Because the storage signal is the one a builder can pass by accident and still fail. Choosing numeric and then loading it into a JavaScript number is the single most likely real-world shape, and a rubric that scored only the schema would award it full marks. The two rows exist as a pair on purpose.
Why display formatting carries the least. It is the most visible signal and the least consequential. A missing formatter is a cosmetic defect that any reader notices in a screenshot. It sits on the list only because its presence is what conceals the six rows above it, which is the trap this axis is named for.
Four postures
- Level 0, approximate. Monetary values rest in floating-point columns or untyped fields. Arithmetic happens in the language's default number. Any correctness is coincidental, and the interface is correct only because a formatter rounds it.
- Level 1, exact at rest. The schema uses an exact type or integer minor units, but values are loaded into ordinary numbers for arithmetic, so exactness stops at the database boundary.
- Level 2, exact through arithmetic. Exact storage is preserved through computation, using integer minor units or an arbitrary-precision type, and a rounding rule is applied at one identified point rather than wherever a value happens to be displayed.
- Level 3, reconciled. Everything in Level 2, plus currency scale is derived per currency rather than assumed, the amount sent to a payment provider is derived from the stored value rather than recomputed alongside it, and an aggregate reconciles against its own line items.
The measurement protocol
The protocol is deliberately boring, which is the point. Nine of these ten steps read static artifacts.
- Generate a fixed reference application from the same prompt suite used for every axis, one generation per builder, no manual edits. The reference feature is a cart with quantities, a percentage discount, a tax line and a total, because that is the shortest specification that forces multiplication, division and rounding.
- Export the project the builder allows you to export.
- Record the emitted schema verbatim and classify the declared type of every monetary column.
- Trace one amount from column to response body and record every type it passes through.
- Locate every rounding operation in the emitted source and record where it sits relative to persistence.
- Search the emitted source for a fixed minor-unit multiplier and record whether the scale is derived per currency or assumed.
- Compare the amount submitted to any payment integration against the amount persisted, and record whether one is derived from the other or the two are computed separately.
- Control step. Record what the interface renders for the same order alongside what is stored. A divergence here is the finding; agreement is not evidence of correctness, only evidence that the formatter and the value happen to match at this magnitude.
- Run the fixed order battery: quantities that force a repeating fraction, a discount that lands on an exact half, and a total large enough to matter, each recorded as stored value and rendered value.
- Publish the raw values alongside the score, so the arithmetic is auditable.
Because every step except the battery reads static files, two runs on the same export produce the same classification. That property is what separates this from render-time measurement, and it is why the axis fits our reproducibility-first methodology.
The named trap: the display-rounding illusion
Each axis in this series names the illusion that hides its defect. Tests that pass without asserting are the green-check illusion. A surface whose only consumer wrote itself is the sole-consumer illusion. A policy that is delivered where it cannot bind is the declared-policy illusion.
Here it is the display-rounding illusion, and it is unusually effective because the concealment is a feature working correctly.
Every monetary value a person sees has been through a formatter on its way to the screen. The formatter's job is to produce two decimal places, and it does that job on a correct value and an incorrect one identically. A stored 19.989999999999998 and a stored 19.99 both render as 19.99. So the entire visible surface of the application, which is the only surface anyone inspects during a build, is incapable of showing the defect. There is no error to catch, no failing assertion, no red state. The screenshot is correct.
The defect surfaces in exactly three places, all of them downstream of the demo: when many rows are summed and the residue accumulates past a cent, when the stored value is compared for equality against a recomputed one and the comparison fails for no visible reason, and when a payment provider is sent a figure derived separately from the one on screen. PostgreSQL warns about the middle case in the same paragraph as the rest: "Comparing two floating-point values for equality might not always work as expected."
This is why the control step is step 8 rather than an afterthought, and why display formatting is the lightest row in the rubric. The formatter is not the fix. It is the reason nobody noticed.
A documented tension worth stating
Two axes in this series would pull a build in opposite directions on one specific remedy, and it is more honest to say so in the pre-registration than to present the axes as independent.
The remedy this axis rewards most heavily is exact arithmetic, and PostgreSQL documents its cost in the same paragraph that recommends it: calculations with numeric values are "very slow compared to the integer types, or to the floating-point types described in the next section." A build that moves every monetary computation into an arbitrary-precision type buys correctness with measurable execution cost.
We do not think this is a reason to weaken the axis, because the integer minor-unit route gets exactness at integer speed and is available to any builder. But a rubric that pretended the trade-off did not exist would be a rubric that had not read its own source.
What this axis is not
- It is not a fintech audit. The reference feature is a cart with a discount and a tax line, not a ledger, a double-entry system or a currency-exchange engine. Anything that needs a settlement model is out of scope.
- It is not a judgement on any builder. No builder is scored, named or assigned a posture on this page, and none will be until the rubric has been through a community-edit window.
- It does not score the display layer's design. Whether a currency symbol sits before or after the number, and whether the locale is right, is a formatting question and belongs to internationalization rather than here.
- It cannot be scored from documentation alone. Every signal in the rubric is read from emitted code and stored values. A builder's marketing page cannot answer any row on this list, which is unusual in this series and is a reason to trust the eventual result more, not less.
- Inherited behaviour is hard to attribute fairly. A builder deploying onto a stack whose default ORM maps decimals conservatively will look better than one deploying onto a permissive default, for reasons that have little to do with generation quality. The protocol records the stack alongside the score for exactly this reason.
Corrections, counterexamples from real deployed builds, and rubric edits are welcome. The most useful thing you can send us is a step 8 observation from your own generated application: what the order page displayed, and what the database actually held for the same row.
References
- PostgreSQL documentation, "Numeric Types," current release. Section 8.1.2 on arbitrary-precision
numericand its recommendation for monetary amounts, section 8.1.3 onrealanddouble precisionas inexact types, the rounding-ties divergence table, and the note on floating-point equality comparison. Read September 7, 2026: https://www.postgresql.org/docs/current/datatype-numeric.html - MDN Web Docs, "Number," JavaScript reference. Number encoding as a double-precision 64-bit IEEE 754 value, the safe-integer range and the mantissa limit, and
toFixed()as a fixed-point string formatter. Read September 7, 2026: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number - TypeScript documentation, "Everyday Types," handbook. The absence of any int or float distinction at the type level. Read September 7, 2026: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
- Stripe documentation, "Supported currencies." The minor-unit convention for API amounts, the two-decimal default, the zero-decimal currency list, and the ISK backward-compatibility case. Read September 7, 2026: https://docs.stripe.com/currencies
Written by
BuilderProof Editorial TeamCite this benchmark
BuilderProof Editorial Team. "Does the invoice total actually add up? A proposed axis for money arithmetic in AI-generated apps (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/does-the-invoice-total-add-up-money-arithmetic-axis-september-2026.
@misc{builderproof-does-the-invoice-total-add-up-money-arithmetic-axis-september-2026,
title = {{Does the invoice total actually add up? A proposed axis for money arithmetic in AI-generated apps (September 2026)}},
author = {{BuilderProof editorial team}},
year = {2026},
month = {sep},
howpublished = {\url{https://www.builderproof.org/benchmarks/does-the-invoice-total-add-up-money-arithmetic-axis-september-2026}},
note = {BuilderProof, builderproof.org}
}Frequently asked questions
What is money-arithmetic correctness for an AI app builder?
It is a proposed BuilderProof benchmark axis, drafted September 7, 2026, scoring whether the application a builder generates stores and computes monetary amounts exactly. It reads the emitted schema, traces one amount from column to response body, and records rounding, currency scale and payment-boundary behaviour. It is a pre-registration: no builder is scored or named in a posture on this page.
Is this the same as the type-safety axis?
No, and the boundary is drawn by TypeScript's own handbook, which states that JavaScript has no runtime distinction between integers and floats and that everything is simply number. A type checker can confirm an amount is a number and cannot ask which kind of number it is. A build can pass a strict compile with every monetary field annotated and still be maximally wrong here.
Why does the database column type change the answer?
PostgreSQL documents that numeric rounds ties away from zero while real and double precision round ties to the nearest even number, and prints a table showing the same round() call returning different results for the same input. It also classifies floating-point types as inexact, meaning some values are stored as approximations, and recommends numeric for monetary amounts.
Is storing amounts as integers enough on its own?
Not by itself, because the scale is a property of the currency. Stripe documents that currencies are two-decimal unless otherwise specified and that API amounts are expressed in the currency's minor unit, so 1000 charges 10 USD while 10 charges 10 JPY. A generated checkout that hardcodes a multiplication by one hundred is wrong by a factor of one hundred for every zero-decimal currency.
What is the display-rounding illusion?
It is the trap this axis is named for. Every monetary value a person sees passes through a formatter that produces two decimal places from a correct value and an incorrect one identically, so a stored 19.989999999999998 and a stored 19.99 both render as 19.99. The visible surface of the application is structurally incapable of showing the defect, which is why the protocol includes a control step comparing what is rendered against what is stored.
Related benchmarks
Type-Safety Posture: A Proposed Axis for Scoring the Code AI App Builders Emit (August 2026)
A candidate BuilderProof benchmark axis that scores the static type safety of the code AI app builders actually emit, measured from the untouched export with no runtime. Rubric, reproduction protocol, and an open call for comment.
Input-Validation and Data-Integrity Posture: A Proposed Axis for Whether AI App Builders Validate Untrusted Input at the Boundary (August 2026)
A candidate BuilderProof benchmark axis that scores whether the code AI app builders emit validates untrusted input at the server boundary, or trusts whatever the client sends. Rubric, four posture levels, an adversarial-payload reproduction protocol, and an open call for comment.
Database migration safety: a proposed benchmark axis for AI app builders (August 2026)
A neutral, reproducible benchmark axis scoring how five AI app builders document database migration and schema-change safety, as of August 2026.