--- name: money-math-review description: Audit every place your code touches money for the four bugs that actually cost real cash: floating-point amounts, rounding applied in the wrong order, mixed or assumed currency, and fees or tax computed off the wrong base. Use before shipping checkout, payouts, refunds, discounts, invoicing, or any change to pricing. --- # Money Math Review ## Install Save this file as `~/.claude/skills/money-math-review/SKILL.md`, or `.claude/skills/money-math-review/SKILL.md` to scope it to one repo. Claude Code auto-discovers it. Invoke with `/money-math-review` or by asking "check my payment math before I ship this". ## Why this exists Money bugs do not throw. They produce a number that looks plausible, ships, and quietly moves real currency in the wrong direction until a human notices a mismatch in a bank statement weeks later. By then you owe reconciliation, not a patch. They are also uniquely unforgiving: a rendering bug annoys someone, a money bug overcharges them. Refunds cost fees, chargebacks cost more, and undercharging is often unrecoverable because you cannot bill retroactively for something you already told a customer was free. Four failures cause most of it. Check them in order, because each one can hide the next. ## Step 1: Find every place money is represented ``` rg -n "price|amount|total|subtotal|fee|tax|discount|payout|balance|refund" --type-add 'code:*.{ts,tsx,js,jsx,py,rb,go,java,php}' -t code ``` For each hit, write down the TYPE and the UNIT. You want a table like: | Where | Type | Unit | Currency | | --- | --- | --- | --- | | `Product.priceCents` | integer | cents | assumed USD | | `cart.total` | number | ??? | ??? | Any row with a `???` is a finding. A money value whose unit you cannot state from the name or its type is a value someone will eventually misread. ## Step 2: Floating point Search for money held in a float or a plain JS `number` used with division: ``` rg -n "(price|amount|total|fee|tax)[A-Za-z]*\s*[*/]\s*" -t code rg -n "parseFloat|toFixed|Number\(" -t code ``` Flag any of these: - Money stored as `float`, `double`, `real`, or written to a DB column of those types. `0.1 + 0.2` is `0.30000000000000004` and it compounds per line item. - `toFixed()` used to "fix" a total before storing it. That converts a representation problem into a stored wrong number. - Currency compared with `===` or `==` after arithmetic. The correct shape is integer minor units (cents) end to end, or a decimal type with explicit precision. Converting to a display string is the LAST step, never an intermediate one. Report the blast radius: a float subtotal that feeds a stored invoice is far worse than one that only renders. ## Step 3: Rounding order This is the subtlest and the most expensive. Find every rounding call: ``` rg -n "Math\.(round|floor|ceil)|\.round\(|ROUND|toFixed" -t code ``` For each, answer: **what is rounded, and at which step?** The classic bugs: - **Rounding per line item, then summing.** Ten items each rounded up can overcharge by up to ten minor units versus rounding the total once. - **Rounding before applying a percentage.** Discount then round, versus round then discount, give different answers. - **`Math.round` on a half value.** Banker's rounding versus half-up is a real accounting decision, not a style preference. If nobody chose, that is a finding. - **Splitting a total across parties** (marketplace payouts, revenue share) without allocating the remainder. `100 / 3` in cents leaves one cent that must go somewhere explicit, or it vanishes and your ledger stops balancing. State the rule the code currently implements, in one sentence, and whether it is written down anywhere. Undocumented rounding is a finding even when correct, because the next change will not preserve it. ## Step 4: Currency ``` rg -n "currency|USD|EUR|GBP|iso4217" -t code -i ``` Check: - Is currency stored next to every amount, or assumed globally? - Can two amounts in different currencies be added anywhere? Find the addition and prove they cannot. - Are zero-decimal currencies handled? JPY and KRW have no minor unit, so "divide by 100 to display" is wrong for them. - Does the payment provider's unit match yours? Stripe uses minor units for most currencies but not all. If the whole app is single-currency, say so explicitly and check that the assumption is enforced somewhere (a constant, a DB constraint), not just true by habit. ## Step 5: Fee and tax base For every fee, commission, or tax calculation, identify the BASE it is computed from, then check the order of operations: - Is the platform fee taken before or after tax? - Is a discount applied before or after tax? - Is a refund refunding the fee too, or keeping it? - On a partial refund, is the fee refunded proportionally? Write the current order as an explicit chain, e.g. `subtotal -> discount -> tax -> platform fee -> payout`. Then check the code matches that chain everywhere it appears. Two code paths that compute the same total in different orders will disagree eventually, and the disagreement will surface as a customer complaint, not a test failure. ## Step 6: Report Report only what you traced in the code. For each finding: ``` [SEVERITY] Where: file:line Now: Should: Costs: Fix: ``` Severity: - **CRITICAL** moves money incorrectly today (wrong amount charged, paid, or refunded). - **HIGH** will move money incorrectly under a reachable condition (a second currency, a partial refund, a specific rounding case). - **MEDIUM** correct today but undocumented or duplicated, so it will drift. End with the operation chain you reconstructed in Step 5. If the codebase handles money correctly, say so and show the chain as evidence. "No findings" is a real result and worth more than an invented one. ## Rules - Never propose a fix you have not traced through the actual code path. - Never say an amount is "probably" fine. Either you followed it end to end or you report it as unverified. - Do not run migrations, do not touch live payment data, and do not issue test charges. This skill reads. - If money is stored as a float and already in production, say plainly that the fix requires a data migration and a reconciliation pass, not just a type change. Understating that is how the second bug gets created. --- From [Toolbay](https://toolbay.ai/product/money-math-review). Free to use, modify, and share. Keep this line and others can find it too.