# Cents

> This skill should be used when code handles money — when the user says "is my money handling correct", "float rounding bug", "the totals are off by a cent", "should I use Decimal", "check my billing/pricing/payment code", "audit financial calculations", or is about to write code that computes prices, totals, tax, balances, or currency. Finds every place money is computed in floating point, where rounding error silently compounds.

- Skill: `lkc-studio/cents` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add lkc-studio/cents`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lkc-studio/cents/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: lkc-studio (https://skillmd.com/u/lkc-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lkc-studio/cents

---


# Cents: money does not belong in float

`0.1 + 0.2 == 0.30000000000000004`. A binary float cannot represent most
decimal fractions, so every arithmetic step on money loses a sliver. Across a
million transactions the slivers add up, the books do not balance, and the bug
is invisible until an auditor finds it.

The fix is old and boring — use `Decimal`, or store integer minor units (cents)
— so this skill is not about the fix. It is about finding **every** place the
mistake was made, which by hand means reading every arithmetic line in the
codebase and asking "is this money?"

## The trap that makes naive scanning useless

`float` is not wrong. It is wrong *for money*. A ratio, a coordinate, a
temperature, an interest rate, an ML weight all belong in float. A scanner that
flags every float drowns the real bugs in a scientific codebase and gets turned
off within a day.

So the question is never "is this a float". It is:

> **Is this a float that represents money?**

`cents` answers it by pairing two signals: a float, and a money name next to it.
Same shape as the rest of this plugin — the construct is only a bug in context.

## Step 1: scan

```bash
scripts/cents.py billing.py           # one file
scripts/cents.py --all src/           # a tree
scripts/cents.py --json payments.py   # for pipelines
```

AST-based, standard library only. It reports each place a float meets a money
name, with the line, the name, and the fix.

```
  billing.py:6  [arithmetic]  (subtotal)
      subtotal = subtotal + item.price * 0.1
      -> float arithmetic on money -- rounding error compounds
  billing.py:8  [rounding]  (total)
      return round(total, 2)
      -> round() on money hides the float error rather than fixing it
```

It stays silent on legitimate floats: `learning_rate = 0.001`, `latitude =
37.77`, `tax_rate = 0.08` (a *rate* is a ratio, not an amount). It also stays
silent on money already done right: `Decimal`, or integer cents.

## Step 2: read each finding by kind

| Kind | What it means | Fix |
| --- | --- | --- |
| `assignment` | a money variable holds a float | assign `Decimal(...)` or an int of cents |
| `arithmetic` | float math on money — the compounding case | move the whole calculation to `Decimal` |
| `float_call` | money parsed via `float(...)` | parse with `Decimal(str(x))` |
| `rounding` | `round(money, 2)` | not a fix — it rounds a value that is still a float; the error already happened |

The `rounding` finding is the subtle one. `round(total, 2)` *looks* like the
fix and is the most common non-fix in billing code. It rounds one float to two
places, but every step before it already lost precision, and the next
calculation reintroduces it.

## Step 3: choose the representation, then convert

Two correct options, and which to use is a real decision:

- **Integer minor units (cents).** Store `1999`, not `19.99`. Fast, exact,
  and impossible to reintroduce a float. Best when the currency's smallest unit
  is fixed and you control the whole path. The catch: every display and every
  external interface must agree on the scale.
- **`Decimal`.** Store `Decimal("19.99")`. Exact decimal arithmetic, handles
  mixed scales and division with an explicit rounding mode. Best when you divide
  (splitting a bill, applying a percentage) or interface with systems that speak
  decimal strings. The catch: never construct from a float — `Decimal(19.99)` is
  already wrong; use `Decimal("19.99")` or `Decimal(str(x))`.

`references/money.md` covers division and rounding modes, currency scale,
databases and serialization, and the interest/percentage cases the scanner
flags but cannot fix for you.

## What it does and does not catch

- **Catches:** literal floats, `float()` calls, and float arithmetic that meet a
  money name in Python source.
- **Does not catch:** money whose variable is *not* named like money (a bare
  `x = 0.1` feeding a total three lines later), floats arriving from a database
  column or JSON already typed as float, or money math inside a library you
  call. Static naming analysis has this ceiling; read the flagged functions in
  full rather than trusting a clean scan of one file.

A clean report means "no money-named float here," not "this program handles
money correctly." For anything financial, read the calculation paths too.

## Resources

- **`scripts/cents.py`** — the scanner. Kinds, money/non-money name signals,
  `--all`, `--json`.
- **`references/money.md`** — Decimal vs integer cents in depth: division,
  rounding modes, currency scale, storage, serialization, and the percentage
  and interest cases.

