Why is my stablecoin payout off by a cent? Minor units, token decimals, and rounding

One-cent gaps between your math and a stablecoin payout come from token decimals, whole-unit rounding, and recomputed fees. How to reconcile each.

A stablecoin payout ends up one cent away from your own math for three reasons: the token has more decimals on-chain than the API amount, some currencies settle in whole units and get rounded down, and recomputed rates or fees use a different rounding rule than the provider. Read amounts from the quote, store integers, and the gaps disappear.

This is a reconciliation guide. The basics of why payment APIs take integers live in stablecoin API quotes explained. Here the question is narrower: your ledger says 6,633.47 and the bank statement says 6,633.46. Which one is wrong?

Key takeaways

  • API amounts are minor units (two decimals for USD, BRL, MXN, EUR). USDC on-chain amounts carry six decimals.
  • MXN, COP, and ARS payins settle in whole currency units, so BlindPay rounds the payer-side amount down to the peso.
  • Don't recompute receiver_amount from the rate. Store what the quote returns.
  • Different rounding rules (half up, half even, truncation) differ by exactly one minor unit on the same input.
  • Exact on-chain values arrive as decimal strings. Keep them as strings until you need math.

Where do one-cent differences come from?

Almost always from one of three conversions. Each has a different fix.

SourceWhat happensTypical gapFix
Token decimalsAPI amount has 2 decimals, on-chain amount has 6Sub-cent dustConvert with integer math, never floats
Whole-unit railsMXN, COP, ARS payer amounts rounded down to a whole unitUp to 0.99 of a pesoShow and store the quoted sender_amount
Recomputed mathYour rate times amount uses another rounding rule1 minor unitRead receiver_amount, don't derive it
Float storage0.1 + 0.2 drifts in binary floating pointRandom, grows with volumeIntegers or decimal strings only

Most teams hit the last two first, because they show up in spreadsheets.

How do API minor units map to on-chain token units?

Two decimals in the API, six on the chain. BlindPay's request_amount, sender_amount, and receiver_amount are integers in minor units: 10000 is 100.00 (payout quotes).

The on-chain side is different. The same docs show a payout quote with sender_amount: 10000 returning a contract.amount of "100000000" for the ERC-20 approve. That's 100 × 1,000,000. So for USDC:

  • API minor units × 10,000 = on-chain base units.
  • On-chain base units ÷ 1,000,000 = USDC.

Two things to notice. contract.amount is a string, on purpose: on-chain integers can exceed what a JavaScript Number holds exactly, which is why MDN documents BigInt for arbitrary-size integers (MDN). And you never need to compute it yourself. Pass the payload from the quote straight to your wallet library. Executing a payout quote on EVM, Solana, and Stellar covers what each chain signs.

Wallet balances can hold dust below one cent (0.000001 USDC is a valid amount). Your ledger works in cents. Expect a few base units of difference between a raw wallet balance and the sum of your recorded payouts, and don't chase it.

Why do MXN, COP, and ARS amounts get rounded down?

Because those rails settle in whole currency units. On a payin quote where currency_type is sender and the currency is MXN, COP, or ARS, request_amount must be a multiple of 100. 10050 fails with request_amount_must_be_a_whole_currency_unit (payin quotes).

The receiver side is where the cent goes missing. If you quote by the stablecoin amount (currency_type: receiver), BlindPay computes the peso amount and rounds it down to the nearest 100 minor units before returning sender_amount. The docs give the reason: a fractional amount would be truncated on arrival at the bank, and the deposit could come back as an invalid payment amount.

Illustrative example. You want a customer in Mexico to fund 500.00 USDC.

LineValue
Requested stablecoin500.00 USDC (request_amount: 50000, currency_type: receiver)
Illustrative rate18.4567 MXN per USDC
Exact conversion500 × 18.4567 = 9,228.35 MXN
sender_amount returned922800 (9,228.00 MXN, rounded down)
Difference0.35 MXN

If your checkout shows "9,228.35 MXN" from your own math, the payer sends 35 centavos more than the quote expects, or your UI and the bank instruction disagree. Show sender_amount from the response. Read receiver_amount too, rather than assuming exactly 500.00 lands.

Why does recomputing the rate give a different number?

Because rounding rules disagree at the half cent. The rate in a quote has more decimals than the currency, so every conversion produces a fraction that someone has to round.

Illustrative example. A payout of 250.00 USDC at a blindpay_quotation of 5.3713 BRL per USDC:

MethodCalculationResult
Exact product250.00 × 5.37131,342.825 BRL
Round half up.825 → .831,342.83
Round half even (banker's).825 → .821,342.82
Truncate.825 → .821,342.82

Three reasonable methods, two answers. Python's decimal module defaults to ROUND_HALF_EVEN and offers eight rounding modes (Python docs), and spreadsheets often default to half up. Neither is wrong. They just aren't the provider's.

The quote already did this math. receiver_amount is the number the bank account receives, after the rate, the flat_fee, any partner_fee_amount, and the billing_fee_amount when billing charges are on. Reconcile against it. If you want to measure the price, compare commercial_quotation with blindpay_quotation as rates, not by re-deriving amounts; how to read an FX quote shows how.

How should you store amounts so reconciliation never drifts?

Integers for minor units, decimal strings for exact values, floats never. JavaScript shows the problem in one line: 0.1 + 0.2 is 0.30000000000000004 (MDN). Sum ten thousand payouts in floats and the error stops being theoretical.

A storage pattern that holds up:

  1. Store the quote's integers. sender_amount, receiver_amount, flat_fee, partner_fee_amount as integer columns, next to the quote_id.
  2. Store the currency with its exponent. ISO 4217 publishes the minor unit for every currency code (SIX, ISO 4217 maintenance agency). USD, BRL, MXN, COP, ARS, EUR, and GBP all use two.
  3. Store rates as decimal strings. "5.3713", not 5.3713. Multiply with a decimal library when you need to.
  4. Keep exact on-chain values as strings. Since October 15, 2026, the managed wallet deposits list returns amount in cents and the exact token_amount as a decimal string (changelog). Match the ledger on amount, keep token_amount for explorer lookups.
  5. Format at the edge. Convert to "R$ 1.342,82" only in the UI.

Webhooks and reconciliation covers matching these rows to payout events.

How do you reconcile a payout against the bank credit?

Match on the quote, not on your math. The receiver_amount on the quote is the expected credit in minor units. Your own record links the payout id (po_...) to the quote_id you executed, and webhooks tell you when it completes.

A reconciliation check that catches real problems and ignores rounding noise:

  • Expected credit = quote receiver_amount. Exact match required.
  • Expected debit = quote sender_amount. Exact match required against your ledger.
  • Wallet balance vs. ledger: allow a tolerance of a few on-chain base units for dust.
  • Any one-cent mismatch on an exact field: check whether someone recomputed instead of reading.

If an exact field is off by more than rounding, that's a real exception. Check the bank details, the payout status, and the tracking fields before anything else. Payout quality metrics shows how to log quoted vs. delivered amounts per payout so these show up on a dashboard instead of in a month-end spreadsheet.

What to do next

Pull last month's payouts, compare your stored amounts with each quote's sender_amount and receiver_amount, and count the one-cent gaps. Every gap points at a place your code recomputes. The payout quotes docs list each amount field.

FAQ