Skip to main content

Preventing duplicate payments when there is no reversal

Wire duplicates get recalled. On-chain duplicates do not. The controls that stop a second payment before it executes, and why idempotency belongs in your payment run.

Plaitr Team8 min read

Duplicate payments are an ordinary accounts payable problem with an ordinary remedy: you notice, you call the bank, and if you are quick enough the second payment is recalled. The error costs time rather than money.

Remove the recall and the same error costs the full amount. On-chain, a duplicate is a completed transfer to a party who is now holding your money and is under no obligation to return it. Most will return it. Some will be slow. A few will not, and you have no mechanism.

So the control has to prevent the second payment rather than detect it. This article covers where duplicates actually originate, and the four controls that stop them.

Where duplicates come from

Almost never from someone deciding to pay twice. The common causes are structural.

The retry after an unclear outcome. A payment is submitted, the interface times out or shows an error, and nobody is certain whether it went through. Someone submits again. This is the single largest source of on-chain duplicates, and it is a failure of feedback rather than judgment.

The same invoice entering the system twice. A supplier emails the invoice and also uploads it to a portal. Two records, two payments, both legitimate-looking in isolation.

Partial payment misread as failure. A payment settles short because of a network fee. The invoice stays open. Someone pays it again in full rather than topping up the difference.

Batch re-run. A payout file fails midway. Rather than identifying which rows succeeded, the operator re-runs the whole file.

Notice that three of these four are caused by uncertainty about what already happened, not by carelessness. That tells you where to put the control.

Control one: idempotency keys

Give every payment instruction a unique key that originates from the invoice, not from the attempt. If an instruction with that key has already been submitted, the system returns the original result instead of creating a second payment.

This is standard practice in payments APIs and underused in internal finance tooling. It directly solves the retry-after-timeout case, which is the biggest cause: the operator can submit again safely, because the second submission returns the first result rather than creating a new transfer.

The key must be derived from the thing being paid. A key generated per attempt protects nothing.

Control two: resolve uncertain states before allowing a retry

The retry problem is really a state problem. The operator does not know whether the payment succeeded.

On-chain this is more answerable than in a banking context, because the ledger is public. A transaction either exists at a block height or it does not, and anyone can check. The information is available; the failure is that the interface does not surface it clearly at the moment of doubt.

Any payment interface should be able to answer "did this go out" without asking a human to search a block explorer. If your current process involves pasting an address into a block explorer to find out whether you paid someone, that is where your next duplicate will come from.

Practically: before any manual retry, require a check against the chain for a matching transfer in the relevant window. Make it a step in the runbook, not a judgment call.

Control three: lock the invoice, not just the payment

Duplicates frequently occur because two payment records point at one obligation. Preventing duplicate payments does not help if the problem is duplicate invoices.

The control is to deduplicate on vendor plus invoice number plus amount at the point an invoice enters the system, and to mark an invoice as paid atomically with the payment instruction rather than after settlement confirms. The window between "payment submitted" and "invoice marked paid" is exactly when a second operator picks up the same open item.

Where the same invoice legitimately arrives through two channels, the second should match to the first rather than create a new record.

Control four: make short payments visibly different from unpaid

A payment short by a network fee is not an unpaid invoice, but in a list of open items it looks identical.

Record the amount sent and the amount received separately, and flag the difference as a fee rather than leaving a shortfall that reads as non-payment. Then set a tolerance below which the invoice closes automatically, because sending a second transaction to settle a few cents costs more than the few cents.

This also keeps your dunning logic honest. Chasing a customer or re-paying a vendor over a network fee neither party controlled is avoidable noise. The mechanics are covered in how to reconcile an on-chain payment to an invoice.

What about batch runs?

Batch payouts concentrate the risk, because a partial failure leaves an ambiguous state across many rows at once.

Three rules make batches safe:

Row-level idempotency. Each row carries its own key, so re-running the file re-attempts only the rows that did not complete. This is the whole answer to the failed-batch problem, and without it there is no safe way to re-run.

A reconciliation step before re-run. Compare the file against what actually settled, by row, before resubmitting anything. The result should be a list of outstanding rows, not a decision to run the file again.

A total check against the threshold. The sum of a batch should be approved as a total, so an accidental double file shows up as a value that exceeds what was approved rather than slipping through as individually unremarkable rows.

Where approvals help and where they do not

A signature threshold is a strong control against unauthorised payments and a weak one against duplicates, because a duplicate looks exactly like a legitimate payment to every approver. The second approver sees a valid invoice, a verified address and a correct amount. There is nothing to catch.

What a multisig does provide is time and separation. A payment requiring two signatures cannot be fired off in a single reflexive click by whoever was watching a timeout spinner, and the second signer is a second chance for someone to notice the invoice number looks familiar.

Treat thresholds as a control against the wrong payment and idempotency as the control against the same payment twice. They solve different problems and you want both. Plaitr enforces the threshold in the smart account itself rather than in software policy, which is covered in security and control.

FAQ

Can you reverse a duplicate stablecoin payment?

No. A confirmed on-chain transfer cannot be recalled or charged back. Recovery depends on the recipient voluntarily returning the funds. This is why duplicate prevention has to be preventive rather than detective, unlike a wire where a recall window exists.

What is an idempotency key in payments?

A unique identifier derived from the obligation being paid, submitted alongside the payment instruction. If the system sees the same key twice, it returns the result of the first instruction instead of creating a second payment. It makes retries safe, which matters because retry after an unclear outcome is the most common cause of duplicates.

How do you know whether an on-chain payment went through?

Check the chain for a transfer matching the amount, recipient and time window. Because the ledger is public, this is definitively answerable, unlike a pending wire. The practical requirement is that your payment interface answers it without someone manually searching a block explorer.

Why did my payment arrive short?

Usually a network fee deducted in transit. Record amount sent and amount received separately so the difference is identifiable as a fee rather than appearing as an underpayment, and set a tolerance so trivial shortfalls close the invoice automatically.

Do multisig approvals prevent duplicate payments?

Not directly. A duplicate looks legitimate to every approver, so the threshold does not catch it. What approvals add is separation and a second pair of eyes. Idempotency keys are the control that actually prevents duplicates.

What should you do next?

Check one thing today: if an operator submits a payment and the screen times out, what does your runbook tell them to do? If the answer is "try again" without a state check, you have an open duplicate risk.

Plaitr runs payouts and payroll from one balance with approvals and limits attached, and posts each transaction to the ledger as it settles so the paid state is never ambiguous. See cross-border payments.

Try Plaitr

Plaitr is business banking for founders who don't live in the US but sell to US customers: self-custodial, multi-currency and stablecoin-native, on one account.