₿ Satoshi Shelf
Pay with Bitcoin · instant delivery

Verify the payment
before the file unlocks.

A seller’s guide to on-chain payment checks, replay attacks, confirmation risk, and the short-lived delivery token that keeps a paid file from becoming a free one.

₿ Lightning checkout ⚡ Instant delivery 🔒 No account needed
Updated 22 October 20268 minute readBy Satoshi Shelf

A screenshot is not proof. A transaction ID is not proof by itself. The only thing that matters is whether a transaction pays your address, for the amount you asked, for this order, and whether you are willing to accept its confirmation status.

Payment verification is the part of a Bitcoin store that looks trivial until it is not. “The txid is on the block explorer” is not enough. A malicious buyer can paste someone else’s transaction, pay the wrong address, underpay by a few thousand sats, or send on the wrong network.

The five checks

CheckWhat to verifyFailure mode
1. ExistenceThe transaction is known to the network or your node.A fake or malformed txid.
2. DestinationAt least one output pays the exact receiving address for this order.Payment to an old, wrong, or attacker-controlled address.
3. AmountThe summed outputs to your address cover the quoted satoshi amount.Underpayment or a stale exchange rate.
4. UniquenessThe txid has not already unlocked another order, and the address or amount binds it to this order.Replay of another buyer’s transaction.
5. FinalityThe confirmation count meets your published threshold.Zero-conf replacement, dropped transaction, or reorg.

A minimal code example

This is deliberately small. It shows the shape of a server-side check, not a complete production implementation. Use your own node or a paid API with a clear SLA for a real store.

const EXPECTED_SATS = 25000;
const STORE_ADDRESS = "bc1q...your address...";
const MIN_CONFIRMATIONS = 1;

async function verifyPayment(txid) {
  const res = await fetch("https://mempool.space/api/tx/" + txid);
  if (!res.ok) throw new Error("Transaction not found");

  const tx = await res.json();

  const paid = tx.vout
    .filter((out) => out.scriptpubkey_address === STORE_ADDRESS)
    .reduce((sum, out) => sum + out.value, 0);

  if (paid < EXPECTED_SATS) throw new Error("Underpaid");

  if (MIN_CONFIRMATIONS > 0 && !tx.status.confirmed) {
    throw new Error("Waiting for confirmation");
  }

  return { txid, paid, confirmed: tx.status.confirmed };
}

The example checks the address and amount. It does not prove uniqueness. A transaction that paid your address for someone else’s order would also pass this function. Add a per-order address, or a unique amount and expiry, then mark the txid as consumed in your database.

Use sats everywhere. vout.value is already in satoshis. Convert the fiat quote to sats once, store the integer, and compare integers. Do not compare floating-point BTC strings.

The traps

The reused transaction

One buyer pays. Another buyer copies the txid. If both orders use the same address and you only check “did this address receive enough?”, both files unlock. Use a fresh address per order, or a unique amount and a short window, and record which txid unlocked which product.

The underpayment

A payment can be one satoshi short and still look successful in a wallet UI. Decide whether you require exact payment or a small tolerance. Whatever you choose, make it explicit and enforce it server-side.

The wrong address

Old addresses, testnet addresses, and addresses from a different wallet all exist. Match the exact string, not a substring or a label. If you rotate addresses, keep a record that maps the address to the order.

The unconfirmed payment

A transaction in the mempool is not final. It can be replaced with a higher fee, dropped, or reorged. For a $4 download, accepting zero-conf may be a rational risk. For a $200 product, it is not. Publish the threshold.

The explorer timeout

When an API times out, do not treat the payment as valid or invalid by accident. Fail closed, show a “we are checking” state, and let the buyer retry. Log the txid for support.

The delivery token

Once the payment passes, issue a token that is:

  • Signed server-side with a secret the browser never sees.
  • Scoped to one product and one order.
  • Short-lived — fifteen minutes is plenty for a download.
  • Checked in constant time before the file is streamed.

The file itself should live outside the public web root. If the plaintext is on a public CDN or in a public repository, a signed URL is security theatre. The token should unlock a server-side read, not reveal a permanent path.

Test plan

  • Pay the exact amount and confirm the file unlocks.
  • Underpay by one sat and confirm it does not.
  • Submit a random txid and confirm it does not.
  • Reuse a txid on a second order and confirm it does not.
  • Try an expired token and confirm it does not.
  • Try a token for product A on product B and confirm it does not.
  • Simulate an explorer timeout and confirm the store fails closed.
  • Test on mobile, where most buyers will open the link.

FAQ

Is a transaction ID enough to prove payment?
No. A txid proves that a transaction exists, not that it paid you, paid the right amount, or paid for this order. Check the output address, the amount, and the confirmation status. If you reuse one address, also make sure the transaction has not already been used for another order.
Should I trust a block explorer?
A public explorer is fine for a small store, but it is a third-party dependency. For production, use your own Bitcoin node or a reputable API with a fallback. Check the response, handle timeouts, and never unlock a file because an API returned an error page.
How many confirmations are enough?
There is no universal number. One confirmation is stronger than zero; more is stronger still. The right threshold depends on the product price and your risk tolerance. Write the rule down and apply it consistently.
Can a buyer reuse another buyer’s transaction ID?
Yes, if both orders use the same receiving address and your store only checks that the address was paid. Use a fresh address per order, or a unique amount and time window, and mark consumed transaction IDs so they cannot be replayed.
What about replace-by-fee and reorgs?
An unconfirmed transaction can be replaced or dropped, and a confirmed transaction can theoretically be reorged. For low-value downloads, a mempool sighting may be an acceptable risk. For high-value files, wait for confirmations and keep the delivery token short-lived.
Where should the verification code run?
On the server, never in the browser. The browser can be modified by the buyer. A serverless function or small backend should check the chain and issue the download token.

Skip the verification bugs

The Bitcoin Storefront Starter Kit includes the checkout, the on-chain check, and the short-lived download token. Read the code, deploy it, and change the wallet.

Keep reading