Never map a Solana confirmation timeout to regenerate/pay-again - #105
Merged
Merged
Conversation
@solana/mpp verifies a transaction-payload credential by broadcasting it (funds move, signature minted) then awaiting confirmation in the same verify() call, with a hardcoded 30s window not exposed through charge(). Under load that window can expire before the network surfaces confirmation even on a healthy production RPC, so verify() throws `Transaction confirmation timeout` on a transfer that may already have landed. handleMppx mapped every unclassified mppx failure to payment_proof_invalid + regenerate_payment_ credential, so the merchant told the agent to pay again for money that already left the wallet (observed live 2026-08-12: on-chain balance delta, no service, a regenerate 402 in hand). classifyMppxFailure now recognizes the confirmation-timeout string and returns payment_pending_confirmation (504, action check_settlement_before_retry) with a user_message telling the buyer to verify settlement before retrying. 504, not 402, so x402/MPP clients do not auto-repay. A confirmation timeout cannot be reliably distinguished from never-landed, so the honest response surfaces the ambiguity rather than asserting either outcome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 12, 2026
Merged
vvillait88
added a commit
to agentscore/python-commerce
that referenced
this pull request
Aug 12, 2026
## Summary Python mirror of agentscore/node-commerce#105. Fixes a live double-charge on the Solana MPP rail. pympp verifies a transaction-payload credential by broadcasting it (funds move, a signature is minted) then awaiting confirmation in the same synchronous verify() call, with a fixed timeout not exposed to callers. Under load that window can expire before the network surfaces confirmation, even on a healthy production RPC (verified: Helius getHealth ok in ~70ms during the failure), so verify() raises on a transfer that may already have landed. `_handle_mppx` mapped every unclassified pympp failure to `payment_proof_invalid` + `regenerate_payment_credential`, so the merchant told the agent to pay again for money that already left the wallet. Observed live 2026-08-12 on the hosted stores: an on-chain balance delta with no service delivered and a regenerate 402 in hand. `classify_mppx_failure` now recognizes the confirmation-timeout reason and returns `payment_pending_confirmation` (HTTP 504, `action: check_settlement_before_retry`) with a message telling the buyer to verify settlement before retrying. 504 rather than 402 is deliberate: x402/MPP clients version-route on status and a 402 auto-triggers a re-pay retry, which is the double-charge this guards. A confirmation timeout cannot be reliably distinguished from never-landed, so the response surfaces the ambiguity rather than asserting success or failure. ## Type of change - [x] Bug fix (no breaking change) - [ ] New feature (no breaking change) - [ ] Breaking change (existing callers must update) - [ ] Docs, tests, or internal maintenance only ## Public API No signature changes. Behavior change on one error path: a Solana MPP settle whose confirmation times out now returns HTTP 504 `payment_pending_confirmation` instead of 402 `payment_proof_invalid` / `regenerate_payment_credential`. Callers that branched on that 402 to auto-repay should treat 504 as submitted-but-unconfirmed and not blindly resubmit. ## Test plan ruff check + format, ty clean, full suite 1844 passing. New tests: the classifier returns 504 `payment_pending_confirmation` (never 402, never `regenerate_payment_credential`) with a do-not-double-pay message on both the plain and status-recovery-failed variants. ## Checklist - [x] Tests cover the new behavior, and the suite passes locally - [x] Lint, format, and type checks pass - [x] Docs and README examples updated if the public surface changed - [x] No secrets, credentials, or personal data in the diff or the tests Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
vvillait88
added a commit
that referenced
this pull request
Aug 12, 2026
## Summary Version bump to 2.7.12 plus the dependency sweep, together so one release carries both. - **`@agent-score/sdk` floor raised to `^2.7.4`**, the version just published from the sdk repo. - **`knip` 6.32.1, `typescript-eslint` 8.67.0.** Nothing else in this manifest is legitimately behind. `eslint` and `@eslint/js` v10 and `typescript` v7 are org-wide holds: v10's new defaults create cross-repo lint churn and a compiler major shifts emitted types, so both move as one deliberate pass rather than a routine bump. The exact-pinned `@x402/*` set and the coupled viem/mppx settle set are already current, and exact pins are invisible to `bun outdated`, so those were read from the manifest directly rather than trusted to the outdated report. This release should land after the Solana confirmation-timeout fix (#105) so the published version carries both. That fix is the reason the stores need this release. ## Type of change - [ ] Bug fix (no breaking change) - [ ] New feature (no breaking change) - [ ] Breaking change (existing callers must update) - [x] Docs, tests, or internal maintenance only ## Public API None. No exported type, function signature, wire format, or response shape changes. The sdk floor moves within its existing caret range, so no caller migration. ## Test plan Full local gate set on the upgraded lock: `bun run lint` clean, `bun run typecheck` clean across the package and the examples project, `bun run test` at 1804 passed / 4 skipped, `bun run knip` clean, and `bun run build` producing both CJS and ESM output. No test changes were needed since no behavior in this repo changed. Reproduce with `bun install && bun run test`. ## Checklist - [x] Tests cover the new behavior, and the suite passes locally - [x] Lint, format, and type checks pass - [x] Docs and README examples updated if the public surface changed - [x] No secrets, credentials, or personal data in the diff or the tests
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
Fixes a live double-charge on the Solana MPP rail, shared by all three hosted stores.
@solana/mpp'sverifyTransactionbroadcasts the transfer (funds move, a signature is minted) and then awaits confirmation in the same synchronousverify()call, with a hardcoded 30s window not exposed throughcharge(). Under load that window can expire before the network surfaces confirmation, even on a healthy production RPC (verified: HeliusgetHealthok in ~70ms during the failure), soverify()throwsTransaction confirmation timeouton a transfer that may already have landed.Checkout.handleMppxmapped every unclassified mppx failure topayment_proof_invalid+regenerate_payment_credential, so the merchant told the agent to pay again for money that already left the wallet. Observed live 2026-08-12 on scaledown and fullenrich: an on-chain balance delta with no service delivered and a regenerate 402 in hand.classifyMppxFailurenow recognizes the confirmation-timeout reason and returnspayment_pending_confirmation(HTTP 504,action: check_settlement_before_retry) with a message telling the buyer to verify settlement before retrying. 504 rather than 402 is deliberate: x402/MPP clients version-route on status and a 402 auto-triggers a re-pay retry, which is the double-charge this guards. A confirmation timeout cannot be reliably distinguished from never-landed (the recoverygetSignatureStatuseswithsearchTransactionHistorylags too), so the response surfaces the ambiguity rather than asserting success or failure.Type of change
Public API
No signature changes. Behavior change on one error path only: a Solana MPP settle whose confirmation times out now returns HTTP 504
payment_pending_confirmationinstead of a 402payment_proof_invalid/regenerate_payment_credential. Callers that branched on that 402 to auto-repay should treat 504 as "submitted, unconfirmed, do not blindly resubmit."Test plan
lint (0 warnings), typecheck including examples, full suite 1808 passing, build clean. New coverage: the classifier returns 504
payment_pending_confirmation(never 402, neverregenerate_payment_credential) with a do-not-double-pay message on both the plain and status-recovery-failed variants; a Checkout-level test proves a capturedTransaction confirmation timeoutyields 504 rather than the regenerate 402.Checklist