Skip to content

Never map a Solana confirmation timeout to regenerate/pay-again - #109

Merged
vvillait88 merged 1 commit into
mainfrom
solana-confirmation-timeout-not-proof-invalid
Aug 12, 2026
Merged

Never map a Solana confirmation timeout to regenerate/pay-again#109
vvillait88 merged 1 commit into
mainfrom
solana-confirmation-timeout-not-proof-invalid

Conversation

@vvillait88

Copy link
Copy Markdown
Contributor

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

  • 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

  • Tests cover the new behavior, and the suite passes locally
  • Lint, format, and type checks pass
  • Docs and README examples updated if the public surface changed
  • No secrets, credentials, or personal data in the diff or the tests

Mirrors the node-commerce fix. pympp verifies a transaction-payload credential
by broadcasting it (funds move, signature minted) then awaiting confirmation
in the same 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, so verify() raises on a transfer that may already have
landed. _handle_mppx mapped every unclassified failure to payment_proof_invalid
+ regenerate_payment_credential, so the merchant told the agent to pay again
for money already gone (observed live 2026-08-12). classify_mppx_failure now
returns payment_pending_confirmation (504, check_settlement_before_retry) with
a do-not-double-pay message; 504 not 402 so clients do not auto-repay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vvillait88
vvillait88 merged commit 52c861a into main Aug 12, 2026
7 checks passed
@vvillait88
vvillait88 deleted the solana-confirmation-timeout-not-proof-invalid branch August 12, 2026 10:57
vvillait88 added a commit that referenced this pull request Aug 12, 2026
)

## Summary

Version bump to 2.5.18 plus the full dependency sweep, together so one
release carries both.

- **`agentscore-py` floor raised to 2.6.4**, the version just published
from the sdk repo.
- **`x402` 2.18.0 to 2.19.0.** This was held on a previous pass for
lacking protocol-field evidence; the evidence was gathered rather than
the hold re-carried. Both wheels were unpacked and compared, and every
protocol-governed field count is identical (`maxAmountRequired` 16,
`x402Version` 105, `decimals` 2300, `payTo` 26, `asset` 1333, `scheme`
1068), so nothing on the wire contract moved. Five files differ, and the
one that matters is a security fix: `x402_http_server_base` normalized
the request path by decoding percent-escapes globally, so a decoded byte
could create a segment boundary the router never saw, and a wildcard
tail carrying a decoded line feed failed to match its own route,
skipping payment verification and settlement. 2.19.0 decodes one segment
at a time, re-escapes any separator a decode yields, and compiles the
route regex with `DOTALL`.
- **`stripe` 15.5.0, `trio` 0.34.0, `ty` 0.0.70.**

2.19.0's only `constants.py` change is Monad Mainnet's USDC name (`"USD
Coin"` to `"USDC"`), a chain we do not settle on. Base mainnet keeps
`"USD Coin"` in both versions, which is the value the EIP-712 domain
hash depends on, so signature verification on our rail is untouched.
That was checked by reading the network block rather than inferred from
the field name.

Six transitives stay behind latest (`websockets`, `pydantic-core`,
`eth-keyfile`, `construct-typing`, `parsimonious`, `jsonalias`). Each is
constrained by its parent (pydantic, solana, web3, sanic, eth-abi,
solders) rather than by anything this repo declares, so forcing them
would override a peer's own resolution. They move when their parents
move.

This release should land after the Solana confirmation-timeout fix
(#109) so the published version carries both.

## 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 `x402` peer moves within its existing `>=2.9,<3`
range, so no caller migration.

## Test plan

Full local gate set on the upgraded lock: `ruff check` and `ruff format
--check` (215 files) clean, `ty check agentscore_commerce/` clean, and
`pytest tests/` at 1841 passed / 4 skipped with coverage 95.36% against
the 95% floor. No test changes were needed since no behavior in this
repo changed; the x402 fix is upstream and covered by its own suite.
Reproduce with `uv sync --all-extras && uv run pytest tests/`.

## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant