Methodology
BuilderProof Editorial Team15 min read46 views

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

Blueprint line diagram: a stack of horizontal ledger rules gathered on the right by a tall summation bracket, with a fine vertical alignment axis running down through them, and one rule sitting minutely off that axis beside a small caliper mark measuring the gap.
Blueprint line diagram: a stack of horizontal ledger rules gathered on the right by a tall summation bracket, with a fine vertical alignment axis running down through them, and one rule sitting minutely off that axis beside a small caliper mark measuring the gap.
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.

TypeScript

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

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

MDN

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

Stripe

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

SignalWeightWhat a failing case looks like
Monetary storage type22Prices, 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 end20An 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 once16No rounding rule exists anywhere; halves resolve differently in different code paths, and the result depends on which layer happened to round first
Currency scale correctness14A 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 agreement12The amount sent to the provider is derived independently of the amount stored, so the two can disagree without anything failing
Aggregate and reconciliation behaviour9Summing 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 value7The 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.

  1. 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.
  2. Export the project the builder allows you to export.
  3. Record the emitted schema verbatim and classify the declared type of every monetary column.
  4. Trace one amount from column to response body and record every type it passes through.
  5. Locate every rounding operation in the emitted source and record where it sits relative to persistence.
  6. Search the emitted source for a fixed minor-unit multiplier and record whether the scale is derived per currency or assumed.
  7. 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.
  8. 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.
  9. 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.
  10. 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

Cite this benchmark

Plain text
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.
BibTeX
@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.