Conversation
A single transfer call was found to produce two independently-settled on-chain transactions when a transport-level retry occurred, because no CLI write call ever passed an idempotency key to the SDK's HTTP client. Thread a deterministic (content + time-window derived) key through every write tool by default, with an optional override so callers can supply their own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
packages/cli/src/mcp/idempotency.ts:deterministicIdempotencyKey(operation, params, windowMinutes=5), deriving a stable SHA-256 key from the operation name, its params, and a 5-minute time bucket.idempotencyKeyinput; when omitted, it falls back to the deterministic key. Compound tools (e.g.create_card,add_spending_category) suffix the key per internal step (:card,:fund) so each underlying write gets its own protected key.transfer,batch_transfer,create_account(funding step),fund_card,add_spending_category(funding step),create_card,create_disposable_card,create_pse_order,create_bank_transfer_order,create_breb_order,topup_via_pse,cashout_to_bank,send_to_breb_key.Why
Live testing surfaced a real double-submission bug: a single
transfercall resulted in two independently-settled on-chain transactions (double the requested amount moved), traced to a transport-level retry after an MCP reconnect. No CLI write call ever passed an idempotency key to the SDK's HTTP client (packages/core/src/http-client.tsalready supports one — it generates a fresh UUID perrequest()call, but that's only reused across that call's own internal retries, not across two genuinely independent calls). This time it was an inter-account transfer to the same user; in other flows (bank cashouts, BRE-B payouts) an unprotected retry could move real money to a third party twice.Test plan
bun run --filter @bloque/cli typecheck— cleanbun test packages/cli— 51 pass, 0 fail (includes newidempotency.test.ts, 5 tests covering stability, param/operation/time-window sensitivity, and digest shape)send_to_breb_keytobbva@pablodorado.com) once this ships, to confirm no duplicate settlement under a forced retry🤖 Generated with Claude Code