AI Code Review for Payments and Money-Handling Code: The Checklist Before a Billing Change Ships
Money bugs do not throw; they reconcile badly a month later. The seven mechanical defects a reviewer can catch in the diff, the five that need project rules, and the rule set to make both run on every PR that touches money.

Money-handling code has a property that most code does not: its bugs are not bugs, they are incidents. A rounding error in a report is a footnote; a rounding error in a payout is a reconciliation meeting, a refund batch and, if it ran for a week before anyone noticed, a conversation with finance about how to explain it. The pull requests that touch this code are often small, which is exactly why they get waved through.
This post is the next in our series on reviewing specific kinds of change with an AI code reviewer, after database migrations and dependency updates. It lists the defects that show up again and again in payment, billing, pricing and ledger code, says which of them a reviewer can catch from the diff and which need repository context or a project rule, and ends with a set of instructions you can give PURA or any other reviewer so the checks run on every PR that touches money.
Why money code is different for a reviewer
Three things make it unusual. The failure modes are quiet: a payment that is off by one cent per transaction does not throw. The correctness rules live outside the code, in currency conventions, tax rules and the documentation of whichever payment provider you use, so a reviewer that only reads the diff has nothing to check against. And the code is rarely exercised by tests with realistic values; test suites love round numbers, and round numbers are where rounding bugs hide best.
That shapes what AI review is good for here. It is excellent at the mechanical patterns, the ones that look wrong on the page once you know what to look for, and it is only as good as its instructions for the semantic ones. The checklist below is split that way.
The mechanical checks: catchable from the diff
1. Floating point anywhere near a currency amount
The classic. 0.1 + 0.2 is not 0.3 in binary floating point, and a price stored as a float or a JavaScript number with decimals will eventually produce a total that is a fraction of a cent off, then get rounded in a direction nobody chose. A reviewer should flag any float, double or decimal-valued number that represents money, any arithmetic on such a value, and any database column of a floating type holding an amount. The accepted fixes are integer minor units (cents, pence, paise) or a decimal type with explicit scale; which one is a project convention the reviewer needs to be told.
2. Rounding without a stated mode
Math.round, round() and friends round half away from zero in some languages and to even in others, and a tax calculation that rounds per line will not match one that rounds the total. A reviewer can flag every rounding call that does not name its mode, every place a per-item and a per-total rounding could disagree, and any division of an amount (splitting a bill, pro-rating a subscription) that does not say where the leftover cent goes. The leftover cent is not a detail; it is the difference between a ledger that balances and one that does not.
3. Currency as an afterthought
An amount without a currency is a number, not money. Flag functions that take an amount and no currency, additions of two amounts whose currencies are not checked to be the same, and hard-coded assumptions of two decimal places. Several currencies have zero minor units and a few have three; code that multiplies by 100 to get cents is wrong for all of them, and a reviewer that knows this will catch it in a line that looks completely innocent.
4. Missing idempotency on anything that moves money
Networks retry. A charge, refund, transfer or payout call that can be sent twice must be safe to send twice, which in practice means an idempotency key derived from your own order or intent identifier, not a random UUID generated at call time. A reviewer can check that every call to a payment provider’s write endpoint carries such a key, that the key is stable across retries, and that the local record of the attempt is written before the remote call, not after, so a crash in between leaves evidence rather than a duplicate charge.
5. Sign and direction errors
Refunds are negative charges until they are not; a credit note, a chargeback and a reversal each have a direction, and code that treats them as positive amounts with a type string is asking for a ledger that adds when it should subtract. A reviewer should flag amounts whose sign depends on a string comparison, any abs() applied to a money value, and negative values passed to a provider API that expects positive amounts with a separate direction.
6. Concurrency on balances
Read a balance, check it, write a new one. If two requests do that at once, both see enough funds and the account goes negative. This is an ordinary race condition, but in money code it has a specific signature a reviewer can look for: any balance update that is not an atomic increment or a transaction with a row lock, and any check-then-act on an amount across two queries.
7. Logging the wrong things
Card numbers, full account numbers and provider secrets in logs are a compliance problem the day they land. A reviewer can flag any log statement, error message or exception that interpolates a request body or a provider response from a payment call, and any new field on a logged object that looks like a PAN, an IBAN or a token. This is the one check that is unambiguously worth blocking on; see blocking vs advisory review for how to configure it.
The semantic checks: context or rules required
The next group cannot be judged from the diff alone, and this is where a diff-only reviewer produces either silence or a confident wrong answer. Each one needs either repository-wide context, so the reviewer can see how the rest of the system treats the value, or an explicit project instruction.
- Who is the source of truth. Your database or the provider? A PR that updates a local status without confirming the provider event, or trusts a client-supplied amount instead of recomputing it server-side, is wrong in a way only the architecture reveals.
- Tax and fee ordering. Discount before tax or after, fee on the gross or the net, VAT inclusive or exclusive. These are business rules; a reviewer told them will enforce them, a reviewer not told them will guess.
- Webhook handling. Events arrive out of order and more than once. Code that moves a payment to
succeededon one event without checking it was not alreadyrefundedneeds the state machine to be visible to the reviewer. - Audit trail. Every change to an amount, a status or a balance should leave a record with who, when and why. Whether that is a trigger, an event table or a log line is a project decision; whether the PR respects it is a review question. Our audit trail post covers the review side of this.
- Tests with realistic values. A test that charges 10.00 and refunds 10.00 proves nothing about rounding. The reviewer can ask for cases with odd cents, three-way splits, zero-decimal currencies and the maximum amount your provider allows.
Turning this into review rules
The mechanical checks are good candidates for custom rules, scoped by path so they fire on payment, billing and ledger directories and stay quiet elsewhere. A rule set that works in practice looks like this, written as instructions rather than regexes:
- Amounts are integer minor units in
int64(or your project’s decimal type). Flag any floating-point value, column or arithmetic representing money. - Every rounding call names its mode and the project’s rounding policy (per line or per total). Flag unnamed rounding and any division of an amount without a remainder rule.
- Every amount travels with a currency. Flag functions that accept an amount without one, additions across unchecked currencies, and hard-coded
100multipliers. - Every provider write call carries an idempotency key derived from our own identifier, and the local attempt record is written first.
- Balance updates are atomic or transactional with a lock. Flag read-check-write on money.
- No request or response body from a payment call is logged. Block on card numbers, account numbers or tokens in any log or error string.
- Tests for money code must include non-round amounts and at least one zero-decimal currency.
Give the semantic checks as project context rather than rules: a short document describing the source of truth, the tax and fee ordering, the payment state machine and the audit mechanism, placed where the reviewer reads it on every PR. PURA reads repository instructions and surrounding code on each review, which is the difference between a reviewer that flags float and one that notices a status transition the state machine forbids; the mechanics are described in what context changes in AI review.
What AI review will not do for you
It will not reconcile your ledger. It cannot tell you that a provider changed its fee structure last Tuesday, or that the tax rate in the fixture is from two years ago, or that a refund path has never been exercised in production. It also cannot see the money: a reviewer reads code, and the strongest control for money code is still a daily reconciliation that compares what you think happened with what the provider says happened. Treat the reviewer as the thing that keeps the mechanical mistakes out of the codebase so the reconciliation only ever finds the interesting ones.
Finally, keep the budget in mind. Money code is a small fraction of most repositories, and the rules above are cheap to run because they are narrow; scope them by path, run them on every PR that touches those paths, and let the general reviewer handle the rest. The per-repository and per-team spend controls in budgets by repository are the tool for making that split explicit.
Frequently asked questions
- Why is money-handling code hard to review?
- Because the bugs are quiet. A payout that is off by a cent per transaction does not throw an exception; it shows up weeks later in reconciliation. The correctness rules also live outside the code, in currency conventions, tax ordering and the payment provider’s semantics, so a reviewer needs those rules spelled out to check against them.
- What can an AI code reviewer catch in payment code from the diff alone?
- The mechanical ones: floating-point values representing money, rounding calls with no stated mode, amounts that travel without a currency, hard-coded two-decimal assumptions, provider write calls without a stable idempotency key, abs() or string-driven signs on amounts, read-check-write on balances, and card or account numbers in log statements.
- What needs project context or custom rules?
- Which system is the source of truth, the tax and fee ordering, the payment state machine for webhook events, the audit mechanism and whether tests use realistic amounts. These are business rules, so give them to the reviewer as repository instructions and path-scoped custom rules rather than expecting it to infer them.
Ready to put your AI review spend on rails?
Install PURA on your GitHub repos and start setting budgets in minutes — not months.
Install PURA for free