The chain can tell you a transaction confirmed. What it can't tell you: why someone asked for it, who approved it, or whether it should have gone out at all.
That gap is why I model a withdrawal as its own record before it is a transfer.
Requested, approved, signed, broadcast, confirmed: explicit states, explicit transitions, each one written down when it happens, not reconstructed later by scanning the chain.
The payoff is everything that happens before broadcast. While a withdrawal is still a request, you can cancel it, hold it for review, reprice it, or batch it with others heading to the same place. After broadcast none of that exists. You have one irreversible fact and a support queue.
Inferring state from the chain also erases the transitions that matter for audit. A confirmed transfer looks identical whether a human approved it, it was auto-approved under a limit, or it was retried after a stuck broadcast. Three different events, three different consequences, and only the record keeps them apart.
I've rebuilt this shape enough times across custodial wallets, exchange payouts and stablecoin rails that I turned it into an open-source project. I called it withdrawals, because the state machine barely changes between them.
Longer write-ups on payments and crypto rails live here: https://polycratia.com/c/60317e09
If you run custodial payouts, I think the interesting part is where you draw the auto-approval line: amount, destination history, or something else...