Agreed on the atomicity: if the send commits and the key does not, a replay is indistinguishable from a first attempt, and that is the expensive direction to fail in.
What bit us sat one layer above that. The key was recorded atomically and the retry still skipped the balance check, because the branch that recognised a replay ran ahead of the checks: same key, different operation. That is why the fix in the test compares type, account and amount before handing back a stored response, rather than anything about how the key is written.
The part I have found nowhere in the literature is the derived key. Ours was built as external_ref + ":hold", so the key for the hold was chosen by whoever supplied external_ref. Scope is written about constantly; what the key was glued together from is not.
Leading with "an idempotency key makes a retry safe" is a good choice, because it is the one most engineers would defend without thinking.
The key only makes a retry safe if the effect and the key are committed atomically. If the send happens and the record of the key does not, you get the worst possible state: the money moved and the system believes it did not.
That is structurally the same bug as at-least-once delivery with a non-idempotent side effect, which is why it turns up far outside payments. The difference is only that everywhere else the duplicate is cheap. Looking forward to the other nine, and the penny that switched off the balance check is a very good opening.