tfpdev
3.3Part 3 · Amounts

Wrong Amount

People rarely pay the exact amount. A fee gets taken on the way, a payment is split in two, someone rounds up. Look for the exact number and money goes missing in your own books. Write down every payment and nothing does.

01Try it

One bill, four ways people actually pay it. Each button sends real test payments. Then compare what each checker wrote down.

NW-2026-119 · 5,800 USDCThe bill · pick how the client pays
Real payments on a test network, in test money worth nothing. Both checkers wait a few blocks, so this page is only about the amounts.15 of 15 payments left this hour
  1. No payments yet
BreaksLooks for the right number
Looks for one payment of at least the invoice amount. Anything else is ignored.
—
Arrived—
In the books—
Lost in the books—
HoldsWrites down every payment
Every payment becomes a line on the invoice. The status is what the lines add up to.
—
Arrived—
In the books—
Lost in the books—

Pick how the client pays. Each button sends real test payments. Then compare what each checker wrote down.

15 payments an hour each. Every payment is real and uses up the lab's test money.

02Step 2

Why writing it all down wins

A payment has no memo

A crypto payment says who, to whom and how much. Nothing says which bill it's for. Matching by amount is the weakest way to guess.

Every payment gets a line

Write each payment down against its bill and add them up. 'Short', 'in two parts' and 'too much' become normal cases with a clear next step.

Small gaps need a rule

Fees shave a little off on the way. Decide in advance how much short is fine, instead of letting the code silently decide.

03Step 3

The code

What runs behind the buttons, in one sentence. The code itself is one click away.

The bad version looks for one payment with the right number and ignores everything else. The good version writes every payment against its bill and lets the total say whether it's paid, short or too much.

Show the code anyway
BreaksBreaks
The version that loses money. Read the grey notes under it, not the code.
match-careless.tsts
for (const d of newDeposits) {  const inv = openInvoices.find((i) => d.amount >= i.amount);  if (inv) await markPaid(inv);  // else: nothing. The money is in the wallet and in no book.}
One deposit, one invoice, matched by the number. Two parts never match, a short payment never matches, and the extra on an overpayment is simply not written anywhere.
HoldsHolds
The version running behind the buttons above.
match-ledger.tsts
await db.query(  "INSERT INTO invoice_lines (invoice_id, tx_hash, amount) VALUES ($1, $2, $3)",  [invoiceFor(d.to), d.hash, d.amount]   // UNIQUE (tx_hash));const paid = await sumLines(invoice.id);const state =  paid === 0n            ? "unpaid"  : paid < invoice.amount ? "partly"     // ask for the rest, or write off the fee  : paid === invoice.amount ? "paid"  :                         "overpaid";  // credit or refund the difference
Every deposit is a line on the invoice it was meant for (one deposit address per invoice). The state is a sum, so partial, short and over all have a name and a next step.
04Step 4

Still curious?

A few more questions people ask. Open what you need.

Why does the careless side still say 'unpaid'?

It looks at each payment on its own. Neither half is enough, so it never says paid, and the client gets chased for money they already sent.

How does the careful side know which bill a payment is for?

On this page there is only one bill. In real life every bill gets its own payment address, so the address says which bill it's for.

What happens to the 15 missing?

The careful side doesn't decide for you. Someone either asks the client for it or writes it off as a fee, and either way it's written down.

Not sure this could happen to you?

That's what the call is for. Tell me how your product works, and I'll tell you what applies to you and what I'd check first.