coinkraft.io
Back to blog
Engineering2026-05-10· 8 min

Forwarding crypto payments: the temporary-address pattern

Every coinkraft.io invoice gets a freshly-derived address it has never used before, and the moment the payment confirms we sweep it onward to the merchant. Three sentences to explain. Three weeks to get right.

Why not just reuse one address per merchant?

Reusing one address is the lazy path and it's wrong for three reasons. Privacy: every customer can see every other customer's payment by looking at the merchant's address history. Reorgs: a 6-block reorg can roll back a payment that you've already credited, and untangling which credit belongs to which order from a shared address is a forensic exercise. Accounting: tying a deposit to an invoice requires a memo or amount-match, both of which fail when two customers pay the same amount.

The Apirone-style fix is per-invoice addresses: derive a fresh address at index N+1 from the merchant's xpub, hand it out, and move on. We use BIP-49 paths so addresses are P2SH-P2WPKH (the boring 3-prefix that every wallet supports).

The state machine

Once the address is handed out, the invoice walks through eight explicit states: awaiting → detected → confirming → ready → broadcasting → completed (or failed, or deferred). Each transition writes a row into the forwarding_runs table with a timestamp, so when something goes wrong at 03:00 we have an audit log instead of a vibe.

The chain-monitor process polls every 1.5s, looks for new transactions to known addresses, and advances the state. When ready, the forwarder constructs a PSBT, ships it to the signer service, broadcasts the signed transaction, and waits for one confirmation before marking the run completed.

What can go wrong

Reorgs are the obvious one — we keep a 6-block confirmation buffer for BTC and re-process the last 12 blocks on every poll. If a tx vanishes from the chain, the invoice flips back to detected and waits for the new chain to catch up.

Dust outputs are the subtle one. If a customer overpays by 100 sats and we have to refund, the refund itself costs 200 sats in network fee — so we'd be sending a negative balance. The fix is a per-coin dust threshold and a 'consolidation' job that batches dust outputs into a single hourly transfer.

← All postsReply by email →
coinkraft.io — The forge for crypto payments