A profit dashboard can look polished and still be wrong by a few cents. One rounding difference rarely matters on its own. Apply it across thousands of orders, refunds, fees, currency conversions, and ad events, and the gap becomes something a merchant can see in a payout report.
Pluto treats that as a data-contract problem, not a formatting problem. Money remains decimal from the moment it enters our system until the moment it is displayed. It never becomes a binary floating-point number in between.
The problem with convenient number types#
Most programming languages make floating-point numbers the convenient default. They are fast and appropriate for measurements, graphics, and many statistical workloads. They are a poor default for money because many ordinary decimal fractions cannot be represented exactly in binary.
That does not mean every floating-point calculation immediately produces a visible error. It means the system starts carrying approximations. Once values are summed, divided into rates, converted between currencies, and rounded at different stages, those approximations can surface in places that are difficult to explain.
For Pluto, the rule is simpler: if a value represents money or a rate a merchant may compare with Shopify, an ad platform, or a spreadsheet, it does not use a float.
The APIs already give us decimal strings#
Shopify returns monetary amounts as strings inside its money objects. Meta follows the same general pattern for spend and attributed purchase value. A value such as "123.450000" arrives with its decimal meaning intact.
We keep that representation at the integration boundary. Shopify money bags are parsed directly into Rust's BigDecimal type. Meta spend can pass through unchanged as the exact string Meta sent, while values that need to be combined are parsed as decimals first. For example, combining click-through and view-through purchase value is decimal addition, not floating-point addition.
This boundary matters. Precision cannot be restored later if it was discarded during the first parse.
Rust carries the exact value#
Rust is useful here because the type choice is explicit. The BigDecimal crate represents a decimal coefficient and scale rather than approximating the value as a binary fraction. Adding 10.10 and 0.20 therefore produces exactly 10.30.
Channel integrations are responsible for turning external payloads into clean financial facts. Shared formulas live somewhere else: a pure Rust calculation crate with no database, network, clock, or UI dependencies. Inputs go in, and decimal results come out without a binary float conversion.
Core analytics metrics have canonical implementations in the calculation crate. The analytics API calls those functions, and the web app receives decimal strings instead of re-deriving the metrics in React. Keeping that path centralized prevents dashboard surfaces from showing different versions of the same metric.
Undefined is not zero#
Precision also means preserving meaning. If a ratio has no valid denominator, the calculation layer returns an undefined result rather than silently substituting zero. ROAS with no ad spend is not 0; it is not defined. The API and interface can then decide how to present that state without changing the underlying math.
The calculation layer also avoids rounding intermediate results. Rounding belongs at an output boundary, where the required display or wire format is known. Avoiding display rounding internally means one consumer cannot dictate the accuracy available to every other consumer.
ClickHouse stores decimals as decimals#
The contract continues at the warehouse boundary. Pluto inserts newline-delimited JSON into ClickHouse and enables its option for reading JSON numbers from strings. Amounts therefore move from BigDecimal or their exact source strings into ClickHouse without an intermediate float conversion.
Financial fact tables use fixed decimal columns. Most monetary values use Decimal(18, 6), while implied currency rates use Decimal(18, 10). The additional scale on rates reduces loss when an amount is converted back to its presentment currency.
Column scale is still a deliberate limit, but it is a visible and consistent one. That is very different from allowing binary approximations to enter the pipeline at an arbitrary point.
Tests focus on financial behavior#
Example calculations are not enough for a financial engine. Pluto uses golden fixtures with exact expected outputs for known profit and margin cases. These comparisons have zero tolerance when the reference result is exactly representable.
Property tests cover broader invariants across generated inputs. Adding two sets of costs should have the same effect as calculating them separately and combining the result. A currency conversion should round trip within its declared scale. A ratio with a zero denominator should remain undefined. These tests check relationships in the math rather than only repeating a few hand-picked examples.
What the merchant gets#
Merchants should not need to know which numeric type Pluto uses. They should notice the result: a refund reconciles, a fee total matches, and a profit figure can be traced back to the same values shown by the source platforms.
Rust helps us enforce the rule, but this is not only a Rust decision. It is an end-to-end agreement between integrations, application code, formulas, storage, and the API. Money enters as a decimal, stays a decimal, and leaves as a decimal string. No layer gets to quietly weaken that contract.
Questions or sharp takes: join Discord or email hello@plutoprofit.com.
