BitAzure
FinTech

Stablecoin Settlement for Merchants: Reconciliation First

Idempotent payment state machines, finality rules, sweeping, statements and exception queues: the unglamorous parts of merchant stablecoin settlement that make the books balance.

BitAzure Engineering · Payments EngineeringPublished Updated 5 min read

Accepting stablecoins from customers looks simple: show an address, watch for a transfer, mark the order paid. Every team that has shipped it learns that the hard part is not detecting a payment but proving, for every payment and every day, that what the chain says, what the merchant's ledger says and what was paid out all agree. Build reconciliation first and the rest of the system falls into place. Build it last and you will spend your first quarter writing scripts to explain discrepancies.

Model the payment as a state machine

A payment has a small set of states and a strict set of transitions. Writing them down and enforcing them in code is the foundation for everything else. A typical machine: created, awaiting funds, detected (seen in the mempool or an unconfirmed block), confirming, confirmed, settled to merchant balance, swept to treasury, and terminal states such as expired, underpaid, overpaid and refunded. Each transition has a trigger, a set of preconditions and a set of side effects, and no code path is allowed to change the state without going through the transition function.

Idempotency by construction

Every transition must be idempotent. Chain watchers restart, webhooks are redelivered, and operators retry. If the same "transfer detected" event is applied twice, the second application must be a no-op. The usual technique is to key every external event by a stable identifier (transaction hash plus log index for a transfer) and record processed identifiers in the same database transaction that updates the payment.

type State = 'awaiting' | 'detected' | 'confirming' | 'confirmed' | 'settled' | 'expired' | 'underpaid';

const transitions: Record<State, Partial<Record<PaymentEvent['type'], State>>> = {
  awaiting:   { transfer_seen: 'detected', timeout: 'expired' },
  detected:   { block_included: 'confirming', reorged: 'awaiting' },
  confirming: { finality_reached: 'confirmed', reorged: 'awaiting', amount_short: 'underpaid' },
  confirmed:  { credited: 'settled' },
  settled:    {},
  expired:    { transfer_seen: 'detected' },
  underpaid:  { transfer_seen: 'detected' },
};

export function apply(payment: Payment, event: PaymentEvent): Payment {
  if (payment.processedEvents.has(event.id)) return payment; // idempotent replay
  const next = transitions[payment.state][event.type];
  if (!next) throw new InvalidTransition(payment.state, event.type);
  return { ...payment, state: next, processedEvents: new Set([...payment.processedEvents, event.id]) };
}

Confirmations and finality are per chain

"Confirmed" means different things on different networks. Some chains have probabilistic finality where the risk of reorg falls with each block; others have explicit finality checkpoints; Layer 2 networks distinguish between sequencer inclusion and settlement on the base layer. Define a finality policy per chain and per amount tier, and make it configuration rather than code. A small payment might be credited at a few confirmations while a large one waits for finality, and the merchant's risk appetite should set those thresholds.

Handle reorgs explicitly. A payment that was confirming can go back to awaiting if the block containing its transfer is orphaned. The state machine above allows that transition; a design that treats confirmation as monotonic will eventually credit a payment that never happened.

Address assignment and matching

Per-payment deposit addresses make matching trivial: any transfer to the address belongs to that payment. They cost an address derivation and, on some chains, a sweep transaction per payment. Shared addresses with memo or reference fields avoid sweeps but fail when customers omit the reference, which they will. On EVM chains, a common compromise is a forwarding contract per merchant with a payment identifier in calldata, or deterministic counterfactual addresses that are deployed only when swept.

Whatever the scheme, matching must handle the awkward cases: partial payments across multiple transfers, overpayments, payments arriving after expiry, transfers of the wrong token to the right address, and dust sent by unrelated parties. Each of these needs a defined outcome and an entry in the exception queue when the outcome requires a person.

Sweeping and treasury

Funds sitting on thousands of deposit addresses are an operational liability. A sweeper consolidates confirmed balances into treasury wallets on a schedule, batching where the chain allows and respecting gas budgets. Sweeps are themselves transactions in the ledger: they move funds between accounts the business controls and must be reconciled like any other transfer. Record the sweep transaction hash against every payment it consolidated so that a treasury balance can be traced back to the customer payments that made it up.

Statements the merchant can audit

A merchant statement lists every payment credited in a period, every fee taken, every payout made and the opening and closing balances, with the invariant that opening balance plus credits minus debits equals closing balance. Generate statements from the ledger, not from the payment table, and include the on-chain transaction references so a merchant's accountant can verify independently. If a statement ever fails its own invariant, that is a production incident, not a rounding issue.

Exception queues

Everything that does not fit the happy path lands in a queue that a person works. The queue is a product, not an afterthought: each item shows the payment, the on-chain evidence, the reason it was flagged and the actions available, such as accept underpayment, refund, re-match to a different order or escalate. Actions are themselves state transitions with an operator identity attached, so the audit trail stays complete.

  • Underpaid and overpaid transfers awaiting a merchant decision.
  • Payments to expired addresses.
  • Wrong-token transfers and dust.
  • Transfers from addresses flagged by compliance screening.
  • Sweeps that failed or were partially executed.
  • Reconciliation breaks between chain, ledger and payouts.

Reconciliation as a daily proof

The reconciliation job compares three views for each period: the sum of on-chain transfers to controlled addresses, the ledger's credits and debits, and the payouts made to merchants. Any difference is a break, and each break is either explained by an exception queue item or is a bug. Run it daily at minimum, alert on unexplained breaks and keep the history. When an auditor or a merchant asks why a number is what it is, the answer should be a query, not an investigation.

A payment system is correct when its reconciliation job is boring. Everything else is a way of getting there.
Architecture 5 min read

Event Sourcing for Financial Ledgers

Immutable events, rebuildable projections, double-entry invariants enforced at write time, replay for audit and the migration pitfalls that catch teams adopting the pattern.

BitAzure Engineering
Security 5 min read

Custody Architectures Compared: HSM, MPC and Multisig

Trust assumptions, operational trade-offs and approval workflows for hardware security modules, threshold signatures and on-chain multisig, and when to combine them.

BitAzure Engineering
Start a conversation

Ready to build your blockchain or fintech product?

Tell us about your platform, timeline and constraints. Our engineers will respond with an architecture-first perspective, not a sales pitch.