Node.js · Go · PostgreSQL · MongoDB · JSON REST · gRPC · Crypto
Contents
Binary floating-point cannot exactly represent most decimal fractions. This is the root of every currency bug.
Store as integer in the smallest unit; divide only for display.
$12.34 → 1234 // scale: 10² $12.345 → 12345 // scale: 10³ 1 BTC → 100000000 // scale: 10⁸ (satoshis) 1 ETH → 10^18 wei // ⚠ exceeds INT64 → use NUMERIC or string
For ETH-scale crypto you need 128-bit integers, NUMERIC, or decimal strings. INT64 overflows at ~9.2 ETH stored in wei.
| Strategy | DB storage | Wire (JSON) | Precision | Crypto-safe |
|---|---|---|---|---|
| Integer (minor units) | INT64 | string | Exact | If scaled |
| Decimal string | NUMERIC(p,s) | string | Exact | Yes |
| Float | FLOAT8 | number | Lossy | No |
| BigDecimal (scaled int) | INT64 + scale | string | Exact | Yes |
Never use FLOAT8, double, or native JS number for currency storage or arithmetic. Errors are silent and accumulate at scale.
Postgres stores NUMERIC in base-10000 chunks. NUMERIC(19,4) is ~12 bytes vs 8 for INT8, and arithmetic is 10–50× slower for bulk aggregations.
| Scale | Impact |
|---|---|
| < 1M rows, OLTP | Negligible |
| 10M+ rows, reporting | Aggregations noticeably slower |
| High-frequency / streaming | INT64 only |
| Crypto (ETH wei) at volume | INT64 overflows — NUMERIC or 128-bit required |
Corruption happens before your code runs — inside JSON.parse itself.
JSON.parse('{"amount": 9007199254740993}')
// → { amount: 9007199254740992 } ← already corrupted, silent
Letting express.json() run globally silently corrupts large numbers before your route handler sees anything.
Push back on the third party to send "amount": "9007199254740993". Best option if you have influence.
const raw = await getRawBody(req);
const safe = raw.toString().replace(
/(?<!["\w])(\d{16,})(?!["\w])/g,
'"$1"' // wrap large numbers as strings before parse
);
const body = JSON.parse(safe);
// ⚠ regex on JSON is fragile — use with good test coverage
// lossless-json — surgical per-field control
import { parse, LosslessNumber } from 'lossless-json';
const result = parse('{"amount": 9007199254740993, "count": 3}');
result.amount.toString() // "9007199254740993" ← exact
Number(result.count) // 3 ← safe, small number
// json-bigint — converts to native BigInt
import JSONBig from 'json-bigint';
JSONBig({ useNativeBigInt: true }).parse('{"amount": 9007199254740993}');
// → { amount: 9007199254740993n }
app.use('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const body = JSONBig.parse(req.body.toString());
});
// Typed struct — safe, no float involved
type Payment struct {
Amount int64 `json:"amount"`
}
// Decoding into interface{} uses float64 — same bug as JS
// Fix: use json.Number
d := json.NewDecoder(r.Body)
d.UseNumber()
| Scenario | Solution |
|---|---|
| You control the third party | Demand strings in the contract |
| Numbers guaranteed < 15 digits | Native JSON.parse is fine |
| No control, numbers can be large | lossless-json or json-bigint |
| High throughput, perf sensitive | Raw buffer regex (benchmark first) |
| Fiat only, minor units (cents) | Likely safe — need $922T to overflow |
Use a layered strategy — not one-size-fits-all.
"amount": "12.34"// Fiat — Google's approach
message Money {
string currency_code = 1; // "USD", "BTC"
int64 units = 2; // whole units
int32 nanos = 3; // 10⁻⁹ fractional part
}
// Crypto at ETH wei scale — nanos insufficient
message CryptoMoney {
string currency = 1;
string value = 2; // "1000000000000000000" (1 ETH in wei)
}
Float is only ever acceptable for approximate display — never for storage or arithmetic. Store the scale explicitly alongside the amount as part of your data contract.