<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[BoltUtil Engineering]]></title><description><![CDATA[BoltUtil Engineering]]></description><link>https://bolt-util.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>BoltUtil Engineering</title><link>https://bolt-util.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 04 Sep 2026 19:00:25 GMT</lastBuildDate><atom:link href="https://bolt-util.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Designing a Safe Reconciliation System for Underpaid Crypto Transactions]]></title><description><![CDATA[A crypto payment does not become easy to reconcile just because it is visible on-chain.
Consider a customer who creates an order for 1.01 USDT and transfers 1.00 USDT. The transaction is valid and con]]></description><link>https://bolt-util.hashnode.dev/designing-a-safe-reconciliation-system-for-underpaid-crypto-transactions</link><guid isPermaLink="true">https://bolt-util.hashnode.dev/designing-a-safe-reconciliation-system-for-underpaid-crypto-transactions</guid><category><![CDATA[Blockchain]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Security]]></category><category><![CDATA[backend]]></category><category><![CDATA[Cryptocurrency]]></category><dc:creator><![CDATA[bolt_util]]></dc:creator><pubDate>Mon, 31 Aug 2026 11:08:10 GMT</pubDate><content:encoded><![CDATA[<p>A crypto payment does not become easy to reconcile just because it is visible on-chain.</p>
<p>Consider a customer who creates an order for <strong>1.01 USDT</strong> and transfers <strong>1.00 USDT</strong>. The transaction is valid and confirmed, but the amount is insufficient. If several active orders share a receiving wallet, matching the transfer to the nearest amount can settle the wrong order.</p>
<p>The safest design is not to make the matcher more confident. It is to limit what the matcher is allowed to do.</p>
<blockquote>
<p>Reconciliation may identify one defensible candidate, but only an explicit merchant decision may settle an underpaid order.</p>
</blockquote>
<p>This article describes the architecture I use for that rule in BoltUtil.</p>
<h2>Start with explicit safety invariants</h2>
<p>Before implementing queries or tolerance calculations, define the properties that must remain true:</p>
<ol>
<li>An ambiguous transfer must never settle an order.</li>
<li>A transfer can settle at most one order.</li>
<li>An order can be associated with at most one accepted reconciliation case.</li>
<li>Original blockchain evidence must remain immutable.</li>
<li>Reconciliation must not delay or replace exact-payment processing.</li>
<li>Repeated scanner events and merchant actions must be idempotent.</li>
<li>A merchant may only view and decide cases belonging to that merchant.</li>
</ol>
<p>These invariants are more useful than a single “matching score.” They give every database constraint, state transition and test a clear purpose.</p>
<h2>Separate transfer evidence from reconciliation decisions</h2>
<p>I model the process with two different records.</p>
<p>The first record represents the observed chain transfer:</p>
<ul>
<li>Network and token</li>
<li>Transaction hash</li>
<li>From and to addresses</li>
<li>Raw token amount</li>
<li>Decimal-adjusted amount</li>
<li>Block number</li>
<li>Confirmation state</li>
<li>First-seen timestamp</li>
<li>Matching status</li>
</ul>
<p>The second record represents a reconciliation case:</p>
<ul>
<li>Merchant and candidate order</li>
<li>Transfer evidence reference</li>
<li>Expected and received amounts</li>
<li>Absolute and percentage difference</li>
<li>Case status</li>
<li>Notification status</li>
<li>Decision actor and timestamp</li>
<li>Rejection or acceptance reason</li>
<li>Audit metadata</li>
</ul>
<p>This separation matters. A merchant decision may change, fail or be retried, but the transaction observed on-chain must not be rewritten to fit that decision.</p>
<p>A unique constraint on the network and transaction identity prevents the same transfer from being imported twice. Additional uniqueness constraints prevent one transfer from being attached to multiple accepted cases.</p>
<h2>Keep the exact-payment path first</h2>
<p>The normal payment path should remain simple:</p>
<ol>
<li>Receive or scan a confirmed transfer.</li>
<li>Persist the transfer evidence.</li>
<li>Try exact order matching.</li>
<li>Complete the order.</li>
<li>enqueue the signed merchant webhook.</li>
</ol>
<p>Only when exact matching does not complete an order should the reconciliation service evaluate the transfer.</p>
<p>In simplified pseudocode:</p>
<pre><code class="language-text">persistTransferEvidence(transfer)

if exactMatchExists(transfer):
    completeExactOrder(transfer)
    return

candidate = findUniqueUnderpaymentCandidate(transfer)

if candidate exists:
    createReconciliationCase(transfer, candidate)
    notifyMerchant()
else:
    keepTransferUnmatched()
</code></pre>
<p>This ordering isolates the new feature from successful payments. Exact payments do not wait for tolerance calculations, email delivery or manual review.</p>
<h2>Filter candidates before comparing amounts</h2>
<p>Amount similarity should be one of the final filters, not the first.</p>
<p>A candidate order must first agree on:</p>
<ul>
<li>Blockchain network</li>
<li>Token or contract</li>
<li>Receiving address</li>
<li>Merchant ownership</li>
<li>Order state</li>
<li>Creation and expiration window</li>
<li>Transfer confirmation requirements</li>
<li>Whether the order or transfer has already been consumed</li>
</ul>
<p>Only then should the service calculate the shortage.</p>
<p>For an expected amount (E) and received amount (R):</p>
<pre><code class="language-text">shortage = E - R
shortageRatio = shortage / E
</code></pre>
<p>A candidate is eligible only when:</p>
<pre><code class="language-text">R &lt; E
shortage &lt;= absoluteLimit
shortageRatio &lt;= percentageLimit
</code></pre>
<p>Using both limits prevents a percentage-only rule from accepting a large absolute loss and prevents an absolute-only rule from behaving inconsistently across order sizes.</p>
<h2>Uniqueness is a decision boundary</h2>
<p>After all safety filters are applied, there are only three valid outcomes:</p>
<ul>
<li><strong>Zero candidates:</strong> retain the transfer as unmatched.</li>
<li><strong>One candidate:</strong> create a review case.</li>
<li><strong>More than one candidate:</strong> retain the transfer as ambiguous.</li>
</ul>
<p>The third outcome is important. Sorting candidates by distance and selecting the first one only hides ambiguity; it does not remove it.</p>
<p>The matcher should therefore return a candidate only when the eligible set has exactly one member.</p>
<h2>Use a small, guarded state machine</h2>
<p>A reconciliation case can use states such as:</p>
<pre><code class="language-text">OPEN -&gt; ACCEPTED
OPEN -&gt; REJECTED
</code></pre>
<p>An accepted or rejected case is terminal.</p>
<p>The acceptance transaction should lock or conditionally update the relevant records and verify that:</p>
<ul>
<li>The case is still OPEN.</li>
<li>The merchant owns the case.</li>
<li>The transfer has not already been consumed.</li>
<li>The order is still eligible for completion.</li>
<li>No competing accepted case exists.</li>
</ul>
<p>Only after those checks should it complete the order and enqueue the normal webhook workflow.</p>
<p>An update shaped like the following is safer than an unconditional write:</p>
<pre><code class="language-sql">UPDATE reconciliation_case
SET status = 'ACCEPTED', decided_at = CURRENT_TIMESTAMP
WHERE id = ?
  AND merchant_id = ?
  AND status = 'OPEN';
</code></pre>
<p>The affected-row count becomes part of the concurrency control. If it is zero, another request or state transition has already won.</p>
<h2>Treat email as notification, not settlement</h2>
<p>Email delivery should never be part of the database transaction that records the transfer or creates the case.</p>
<p>The durable sequence is:</p>
<ol>
<li>Commit transfer evidence and the reconciliation case.</li>
<li>Record that a notification is pending.</li>
<li>Send the email asynchronously.</li>
<li>Record success or retry information.</li>
</ol>
<p>If the email provider is unavailable, the case still exists in the merchant dashboard and can be retried safely. Payment correctness must not depend on an external mail server.</p>
<h2>Preserve tenant boundaries</h2>
<p>Reconciliation endpoints need the same ownership rules as normal order endpoints.</p>
<p>A case lookup should be scoped by both case ID and authenticated merchant ID. Accept and reject operations should never trust a merchant identifier supplied by the browser.</p>
<p>Administrative visibility, if required, should use a separate privileged route and generate an audit event.</p>
<h2>Test failure paths, not only successful matching</h2>
<p>The most valuable tests cover situations where the system must refuse to act:</p>
<ul>
<li>No eligible order</li>
<li>Two equally valid candidates</li>
<li>Correct amount but wrong network</li>
<li>Correct amount but wrong token contract</li>
<li>Transfer outside the order window</li>
<li>Replayed scanner event</li>
<li>Two simultaneous accept requests</li>
<li>Order completed before manual acceptance</li>
<li>Email failure after case creation</li>
<li>Merchant attempting to access another merchant’s case</li>
</ul>
<p>A green unit test suite is still not real payment acceptance evidence. Each supported chain also needs a minimal end-to-end test covering a real transfer, confirmation depth, persistence, order completion and webhook delivery.</p>
<p>In my current deployment, the TRC20 path has processed roughly 50 real completed orders. Other supported network integrations are being validated separately before I describe them as production-proven.</p>
<h2>What this architecture achieves</h2>
<p>The system does not pretend that an underpayment is automatically correct.</p>
<p>Instead, it:</p>
<ul>
<li>Preserves the chain evidence</li>
<li>Finds only a unique and defensible relationship</li>
<li>Refuses ambiguous matches</li>
<li>Keeps exact-payment processing unchanged</li>
<li>Makes merchant approval explicit</li>
<li>Records every terminal decision</li>
<li>Reuses the normal order and webhook completion path</li>
</ul>
<p>I am implementing this design in <a href="https://boltutil.com/">BoltUtil</a>, a non-custodial crypto payment API where funds move directly to the merchant wallet.</p>
<p>The broader lesson applies beyond cryptocurrency payments: when automation cannot prove identity or intent, its safest output is a reviewable candidate—not an irreversible business decision.</p>
]]></content:encoded></item></channel></rss>