Each executePurchase POST mints a fresh execution row + token (newPendingExecution, handler_purchases.go:1174-1192); the fan-out path fires N parallel POSTs (frontend/src/app.ts:438-446). The Send-for-approval button is only disabled AFTER the confirm dialog resolves (app.ts:346-349). A double-click or a retried network call creates duplicate pending executions for the same recs, each independently approvable -> double spend.
Fix: a submit-time idempotency key (e.g. hash of the rec set + account + actor within a short window) so a duplicate submit returns the existing pending execution instead of creating a second. Disable the button before the network call too.
Dedup: distinct from #636/#641 (commitment-layer ClientToken) and #632 (strand) — this is duplicate EXECUTION-ROW creation at submit time. New. Surfaced in the purchase-workflow trace.
Each
executePurchasePOST mints a fresh execution row + token (newPendingExecution,handler_purchases.go:1174-1192); the fan-out path fires N parallel POSTs (frontend/src/app.ts:438-446). The Send-for-approval button is only disabled AFTER the confirm dialog resolves (app.ts:346-349). A double-click or a retried network call creates duplicate pending executions for the same recs, each independently approvable -> double spend.Fix: a submit-time idempotency key (e.g. hash of the rec set + account + actor within a short window) so a duplicate submit returns the existing pending execution instead of creating a second. Disable the button before the network call too.
Dedup: distinct from #636/#641 (commitment-layer ClientToken) and #632 (strand) — this is duplicate EXECUTION-ROW creation at submit time. New. Surfaced in the purchase-workflow trace.