Build1 publisher2 min readPublished
The cryptomoney parser rejects a nine-decimal BTC amount until the call site names a rounding mode
A dev.to post puts the precision check in the parser and refuses Python floats outright. The cost of adopting that lands at the HTTP boundary, where the decoder has to hand over the sender's digits as text.
The Engineer · Build desk
What happened
- A dev.to post describes cryptomoney, a Python library that puts the asset precision check in the parser and makes refusal the default.
- parse_amount("1e-9", BTC) raises ParseError with a message saying the input needs 9 decimal places, so an over-precise amount never reaches a balance by default.
- The same parser accepts messy real payload text, including whitespace, thousands separators, exponent notation and a symbol glued to the number, because those forms turn up in production bodies.
- The author says the common alternative is to truncate the value into a NUMERIC(18, 8) column, where the difference later surfaces as reconciliation drift nobody can attribute.
- Adopting the library means the HTTP boundary hands over raw text, using a decoder that keeps numbers as strings or a schema that types the amount field as a string.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Product owners have to settle what a floored quote and a withdrawal do with an unrepresentable amount before the code runs, because the rounding mode is now an argument someone types.
- cost The team that owns intake pays the adoption cost, one decoder or schema change at a time, and the guarantee only covers the paths they got to.
- constraint A service that already receives amounts as JSON numbers cannot be retrofitted by a stricter parser, because the double it holds no longer contains the sender's digits.
- capability With every Money validated at construction, a mixed-asset operation fails as CurrencyMismatch at the point of the addition instead of quietly producing a wrong total.
The float refusal will look like pedantry at review, and it is the part I would keep. A JSON decoder turns the sender's text into a binary double before your handler runs, so the original digits are gone, and with them any way to say whether the sender wrote an amount the asset can express [16]. "A parser that accepts floats is not parsing, it is laundering a decision that was already made badly upstream," the post's author wrote [17]. `parse_amount(0.5, BTC)` raises `TypeError` [10]. `Money(0.1, BTC)` raises the same thing, so a float cannot enter a balance through the constructor either [18].
BTC is divisible into eight decimal places, which is what `Money("0.000000001", BTC)` reports when it raises `ValueError` [19]. The smallest amount BTC can express is therefore 0.00000001, and the `1e-9` in the examples is a tenth of that [24]. A `NUMERIC(18, 8)` column has a scale of eight as well [25]. It accepts the value, drops the ninth digit, and returns no error.
The second branch is where the design sits. Pass `rounding=ROUND_DOWN` on the same input and `parse_amount("1e-9", BTC)` returns 0.00000000 BTC [9]. A withdrawal endpoint that ships that flag accepts the request and moves nothing. The post keeps the mode at the call site for that reason: a quote engine flooring a displayed rate and a withdrawal endpoint that must not create value out of nothing are different call sites with different answers, and neither should inherit a default written by a library author [15]. "Precision belongs to the asset, and rounding is a policy you declare before you compute, not a residue you discover afterwards," the author wrote [4].
The author calls the boundary change "A small amount of friction in exactly one place" [23]. In a system that takes amounts from a form field, a JSON body from an exchange API, a row in a payout file and a provider webhook, it is one place per intake path [1]. The parser protects the paths you changed.
Two things have to be true for this to transfer. Your amounts still have to exist as text when validation runs; if intake converts to integer satoshis first, the representability question was answered upstream [2]. And the assets in your product have to be assets the library knows, because `parse_money("0.5 XMR")` raises `UnknownAsset` [12]. The post is one practitioner's account, dated to 2018 and covering custodial BTC and ETH wallets, a fiat-to-crypto onramp, exchange order books and stablecoin rails in daily use [5], and it does not quantify how much reconciliation drift the check prevented.
What to watch
- Whether the project documents how to register an asset it does not ship, given parse_money("0.5 XMR") raises UnknownAsset.
- Whether anyone publishes measured reconciliation drift before and after moving the precision check from the column into the parser.
- A released version and asset table for cryptomoney, showing which assets and precisions are known by default.