---
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 — from Toolbay
  https://toolbay.ai/product/money-math-review
  Free to use, modify, and share. Keep this line and others can find it too.
-->


# 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] <one line: what is wrong>
  Where:    file:line
  Now:      <what the code computes today, concretely>
  Should:   <what it should compute>
  Costs:    <who loses money, and roughly how much per occurrence>
  Fix:      <smallest change that is actually correct>
```

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.
