feat(commerce 1.3)!: kill x402 Solana, add MPP solana/charge + classify+extraWarnings + x402scan discovery#8
Merged
vvillait88 merged 19 commits intomainfrom May 5, 2026
Conversation
…or_error' Today processX402Settle only wraps settlePayment in try/catch. The three earlier facilitator-touching calls — buildPaymentRequirements, enrichExtensions, processPaymentRequest — bubble exceptions unchecked, so a facilitator that throws on a misconfigured network (e.g. Coinbase CDP rejecting Solana devnet) lands in the merchant's outermost catch as an opaque 500. Add a new 'facilitator_error' tagged variant to ProcessX402SettleResult with a 'step' discriminator (build_requirements | enrich_extensions | process_payment_request) and the original error for diagnostics. Wrap all three calls. Merchants get structured 503-with-recovery instead of 500. Stays facilitator-agnostic — no vendor-name validation, no auto-select. Merchants pick whichever facilitator they want (Coinbase CDP, public x402.org, Nevermined, self-hosted); the SDK now just surfaces facilitator failures cleanly regardless of which one was chosen. Test plan: - 8 new tests in tests/payment/x402_settle.test.ts covering all success + sad paths - Full suite: 638 passed Bumps to 1.3.0 (minor: additive variant on the result union, no breaking change to the success path or existing failure phases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maps the discriminated union from processX402Settle to a recommended
{status, code, message, nextSteps} envelope so merchants stop sniffing
facilitator error message strings to decide between 400 / 503 / 500.
Phase mapping:
- verify_failed -> 400 payment_proof_invalid / regenerate_payment_credential
- facilitator_error -> 503 payment_provider_unavailable / try_different_rail
- settle_failed -> 503 payment_provider_unavailable / retry_or_swap_method
(retry_after_seconds: 10)
- no_requirements -> 500 payment_internal_error / contact_support
Returned object is intentionally facilitator-agnostic and never carries
the raw error; merchants log result server-side and return the classified
envelope to the consumer.
Updated JSDoc on each non-success variant of ProcessX402SettleResult to
spell out the log-raw / return-controlled posture and point at the
helper. Replaced the brittle 'phase' field leak in the README example
with the helper. Mirrored the helper mention into the package-table
docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
Describe the helper neutrally as a controlled-envelope mapper that decouples merchants from facilitator-specific error text, instead of naming the prior antipattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t-info, x-guidance, siwx)
Closes the discovery-substrate gap with x402scan: every commerce-SDK merchant
now emits everything x402scan's strict parser needs, no per-merchant boilerplate.
- buildWellKnownX402(): emits {version: 1, resources: ["METHOD /path"]} for /.well-known/x402
- xPaymentInfoExtension(): per-operation x-payment-info with fixed/dynamic price + protocols (x402, mpp)
- xGuidanceExtension(): info.x-guidance prose blob for OpenAPI top-level
- siwxSecurityScheme(): http bearer SIWX entry for components.securitySchemes
- agentscoreSecuritySchemes() now includes siwx alongside OperatorToken + WalletAddress
- /.well-known/x402 added to defaultDiscoveryPaths so the noindex middleware lets it through
Smoke verified end-to-end: spun up an http server emitting all the new docs and
ran @agentcash/discovery against it. Output:
Source: openapi
Routes: 3
POST /purchase paid 0.10 USD [x402, mpp]
POST /quote paid 0.01-5.00 USD [x402]
GET /whoami siwx
Guidance: 29 tokens
x402scan classifies fixed-price, dynamic-price, multi-protocol, and SIWX-only
routes correctly. Only warning is missing favicon (cosmetic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…al Solana path x402 Solana was structurally broken against any payTo whose USDC ATA wasn't pre-warmed. The upstream `@x402/svm` `ExactSvmScheme` tx-builder doesn't include a `createAssociatedTokenIdempotent` instruction, so SPL transfers fail simulation with `InvalidAccountData` whenever the destination ATA doesn't exist (Stripe- multichain rotating addresses, fresh wallets, etc.). Upstream's stance per x402-foundation/x402#1020 is that ATA pre-creation is the seller's responsibility by design (rent-drain attack vector); they will not patch. The cleaner architectural answer is MPP `solana/charge`. The buyer's tx-builder includes idempotent ATA-create before TransferChecked, so payments work against any payTo, and the spec is what Stripe officially routes Solana through per docs.stripe.com/payments/machine. Changes (BREAKING): - Drop `x402-solana-mainnet` / `x402-solana-devnet` rails from the rail registry - Drop SVM from `validateX402NetworkConfig` (now base-only) and `verifyX402Request` (single `acceptedNetwork`, not a `{base, svm}` object) - Drop SVM from `createX402Server` rail union - Drop the SVM extraction branch in `extractPaymentSigner` - Add `mpp-solana-mainnet` / `mpp-solana-devnet` rails (method='solana') - Add `solana` rail to `createMppxServer` wiring `@solana/mpp/server` charge - Rename `x402_solana` -> `solana_mpp` field across challenge builders and discovery (accepted_methods emits `solana/charge`, agent_instructions/skill_md/llms_txt surface MPP Solana terminology) - New `SolanaMppMethodEntry` type with optional `fee_payer_key` - Drop `@x402/svm` from devDependencies; add `@solana/mpp` + `@solana/kit` as optional peer deps - Trim implementation noise (ATA-create mechanics, settle internals) from buyer-facing surfaces (skill.md, llms.txt, agent_instructions, how_to_pay) per feedback that those are agent-actionable, not protocol explainers - Bump 1.3.0 -> 1.4.0 - Update README + examples + CLAUDE.md 655 tests pass, lint + typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
…adata
Tightens the audit findings post-1.4.0 release:
- README quick-start sample updated to the new single `acceptedNetwork`
signature (was the dropped `acceptedNetworks: { base, svm }` object)
- examples/README + CLAUDE.md per-example descriptions advertise Solana
via MPP `solana/charge`, not x402 SVM
- api-provider.ts inline comment clarifies which header carries which
protocol now that x402 is base-only
- well_known_mpp test passes through `x402.networks: ['base']` instead of
`['base', 'solana']`; merchant builders should advertise base only
…s hook verifyX402Request gets a dedicated branch for incoming x402 credentials on a solana:* network. Instead of the generic "Unsupported x402 network" message, the response points the client at the `solana/charge` rail in the 402 challenge so an agent on a stale x402 SVM client can recover with one re-sign. Both the error message and next_steps stay behavior-only: no internal terminology, no CLI vendor references, no protocol-handshake mechanics, no infra disclosure. buildAgentInstructions gets an `extraWarnings` field so merchants can append per-order rail-availability notes on top of the SDK's protocol-footgun defaults (without overriding them). Set `warnings` directly to override entirely; `extraWarnings` is ignored in that case. - src/payment/x402_validation.ts: new solana:* branch in the network-mismatch check - src/challenge/agent_instructions.ts: BuildAgentInstructionsInput gets `extraWarnings`; default-warnings concat covers both rails + extras - tests/payment/x402_validation.test.ts: assertion updated for the new Solana hint - tests/challenge/agent_instructions.test.ts: two new tests for extraWarnings + the override case
Public-package source describes behavior, not internal state names or named-customer business attributions. Sweep across docstring examples and agent-visible warnings: - Removed "pending_identity" (internal order-state column value) from the default warnings emitted by buildAgentInstructions + buildLlmsTxt; agents see "the order will not complete" instead of an internal state name - Genericized docstring examples in identity/a2a.ts, identity/ucp.ts, discovery/skill_md.ts, challenge/order_receipt.ts: "Martin Estate Wine Concierge" + "agents.martinestate.com" + "wine-purchase" placeholders replaced with "Example Merchant" / "agents.example.com" / generic skills - Removed the "(Martin Estate today; Commerce7 / WooCommerce / Shopify plugins tomorrow)" forward-looking rollout note from order_receipt.ts Mintlify worked-example pages and the public marketing site keep referencing Martin Estate by name; that's allowed at the marketing/URL level per the disclosure-posture rules.
…ocstring buildContactSupportNextSteps usage example used a real customer's support email address. Genericize to support@example.com per the disclosure-posture rules; merchants supply their own address at the call site.
Public-package tests are part of the public surface (visible on the npm tarball + GitHub source). The hono + a2a tests were using "Martin Estate" / "https://agents.martinestate.com" / "wine-purchase" as fixture values; that bakes a public reference customer's domain into the SDK source. Swap to "Example Merchant" / "https://agents.example.com" / "product-purchase".
Public-package source + tests describe behavior, not threat model. - src/core.ts: drop "malicious merchant phishing agents into attacker- controlled endpoints" rationale from the CANONICAL_AGENTSCORE_API comment; the constant is hardcoded; the why doesn't need to ship - src/stripe-multichain/pi-cache.ts: replace "prevents agents from sending payment to an attacker-controlled address" with "validates the credential's deposit address against the addresses the merchant has minted" - src/payment/x402_validation.ts: replace "defends against agents replaying credentials against attacker-controlled deposit addresses" with the behavior-only equivalent Tests: genericize remaining customer references in tests/_response.test.ts, tests/identity/ucp.test.ts, tests/payment/directive.test.ts, and the entire tests/discovery/skill_md.test.ts fixture set. Swap "Martin Estate" / "martinestate.com" / "wine-purchase" / "martin-estate-wine-commerce" for "Example Merchant" / "example.com" / "product-purchase" / "example-merchant-commerce".
…ub remaining wine ref - tests/signer-match.test.ts: rephrase the buildAgentMemoryHint test name + comment from "prevent cross-merchant phishing / malicious merchant" to "emits the canonical AgentScore API regardless of merchant baseUrl". Behavior is the same; the rationale narration goes away. - tests/express.test.ts: replace remaining 'wine purchase' context fixture with 'product purchase'
Lands the cycle's commerce SDK changes (kill x402 Solana + add MPP solana/charge + classify+extraWarnings + x402scan discovery + internal- disclosure scrubs) as the next minor release after 1.2.0.
…Request A misformatted credential carrying `SOLANA:5eyk...` or `Solana:5eyk...` would fall through to the generic 'Unsupported x402 network' message instead of the targeted Solana hint that points at the `solana/charge` rail. CAIP-2 spec is lowercase by convention, but defensive code costs one `.toLowerCase()` and rescues malformed inputs.
…tSigner Anticipatory: when an MPP credential's `source` field is a Solana CAIP-10 DID (`did:pkh:solana:<genesis-base58>:<address-base58>`), extract the base58 address with network='solana'. Mirrors the existing EVM branch. Why anticipatory: `@solana/mpp/client` does not currently set `Credential.source` when serializing a `solana/charge` credential, even though the mppx Tempo plugin does set `did:pkh:eip155:...` for tempo. So this matcher is dead code today; it activates as soon as upstream sets source, or as soon as a merchant/client adds source themselves. Without this matcher, Solana MPP settles produce `capturedSigner=null`, which silently skips `captureWallet` (no operator-wallet link forms on Solana) and weakens wallet-auth signer-match enforcement vs the EVM rails. Test: tests/signer.test.ts now covers the Solana DID happy path; existing EVM-DID and non-DID null cases stand.
…l mode)
Closes the wallet-signer-match gap on Solana MPP `solana/charge`. When the
mppx credential's `source` field is unset (current `@solana/mpp/client`
behavior), fall back to decoding the credential's signed-tx payload and
read the SPL `TransferChecked` authority field. The authority is the
source-ATA owner, which is the buyer's wallet.
Pull mode only (`payload.type === 'transaction'`). Push mode
(`payload.type === 'signature'`) returns null because recovery would
require an RPC fetch by signature; pay uses pull mode by default and
no consumer in the stack opts into push today.
Why this matters: without signer recovery, the gate's wallet-auth check
on Solana could not enforce that the claimed `X-Wallet-Address` matches
who actually paid. An agent claiming a KYC'd wallet could pay with an
unverified one. Now the recovered Solana base58 address feeds
`verifyWalletSignerMatch` the same way the EVM EIP-3009 `from` does, so
operator-equivalence enforcement is uniform across rails.
Implementation:
- `@solana/kit` is dynamic-imported, matching the existing `mppx` pattern;
optional peer dep already declared in package.json (>=6.5.0).
- Walks instructions for SPL Token Program or Token-2022 with the
`TransferChecked` discriminator (12), then reads `accountIndices[3]`
(authority slot) and looks it up in `staticAccounts`.
- Failure paths (missing peer dep, malformed tx, no TransferChecked,
push-mode payload) all return null cleanly so the existing fall-through
to x402 EVM still works.
Tests: 660 passing (up from 658). New cases:
- pull-mode Solana credential with TransferChecked → returns
{ address: <base58>, network: 'solana' }
- push-mode Solana credential (`type: 'signature'`) → returns null
…doc updates Two issues from the round-5 review: 1. Account-index overflow on v0 transactions with address lookup tables. Solana v0 txs can carry instruction account indices that resolve via lookup tables; staticAccounts only holds the static set. If `accountIndices[3]` (TransferChecked authority) points outside staticAccounts, the previous code returned the wrong address (or `undefined`) silently. Now: bounds-check, log a warning, skip cleanly. The `@solana/mpp/client` builder uses static accounts only for fresh SPL transfers, so this is defensive against future tx shapes rather than a current-day bug. 2. Multi-recipient `splits` clarification. The loop returns the FIRST matched TransferChecked authority. For splits, the buyer signs ONE tx with N TransferChecked instructions all sharing the same authority (their wallet), so first-match is correct. Added a comment to make the assumption explicit so a future maintainer doesn't read the loop as buggy. Doc updates pulling the Solana signer-recovery work into the canonical descriptions: - README.md `extractPaymentSigner` snippet now mentions x402 EVM, Tempo MPP, and Solana MPP (DID-or-tx-decode) coverage. - CLAUDE.md wallet-signer-match paragraph spells out the three recovery paths and the optional `@solana/kit` peer dep.
…able, throw) Three new tests for `extractSolanaSignerFromCredential`: 1. `@solana/kit` peer dep stub missing required exports → getBase64Codec/getTransactionDecoder/getCompiledTransactionMessageDecoder undefined; returns null without throwing. 2. SPL TransferChecked authority resolves through an address lookup table (accountIndices[3] >= staticAccounts.length); skips with a warning log instead of returning a wrong address. 3. @solana/kit decoder throws on malformed tx bytes; outer try/catch catches and logs, returns null cleanly. Closes the branch-coverage gap that was blocking PR CI (89.95% → 90.1%, above the 90% threshold).
…9.1 → 8.59.2 Within-semver patch bumps. eslint 9 → 10 + @eslint/js 9 → 10 are available but held back as a separate major-bump decision (eslint 10 removed several deprecated APIs and changed the rule meta-format; needs its own validation pass). Tests: 663 vitest passing, lint + typecheck clean.
vvillait88
added a commit
to agentscore/pay
that referenced
this pull request
May 5, 2026
## Summary Pairs with agentscore/node-commerce#8 + agentscore/python-commerce#7 + agentscore/martin-estate#56. \`--chain solana\` no longer registers \`@x402/svm\`'s \`ExactSvmScheme\`. It now registers \`@solana/mpp/client\`'s \`charge\` method on \`Mppx\` and pays via MPP \`solana/charge\`. Reason: \`@x402/svm\`'s tx-builder omits the idempotent \`createAssociatedTokenAccount\` instruction, so SPL transfers fail against any payTo whose USDC ATA isn't pre-warmed (every Stripe-multichain rotating deposit address). Upstream won't fix this per x402-foundation/x402#1020 (rent-drain attack vector). ## Changes - Drop \`@x402/svm\` dep - Add \`@solana/mpp\` client peer dep - \`wallet.chain === 'solana'\` now goes through \`payViaMpp\` (was \`payViaX402\`) - \`payViaMpp\` registers \`solanaCharge({signer, rpcUrl})\` for Solana wallets, \`tempo({account})\` for Tempo - \`Protocol\` is \`'x402'\` for base, \`'mpp'\` for Solana + Tempo - \`payViaX402\` throws \`unsupported_rail\` for non-base chains - Everything else unchanged (passport attach, \`--max-spend\`, idempotency, retries, dry-run) Agent UX is identical: \`agentscore-pay pay POST <url> --chain solana ...\` continues to work; pay handles the protocol switch transparently. ## Test plan - [x] 352 tests pass (vitest) - [x] Local dry-run shows \`protocol: mpp\` for \`--chain solana\` - [x] Local live-run reaches simulate against martin-estate (broadcast deferred on devnet RPC reliability) - [ ] On-chain settle smoke (mainnet or alternate devnet RPC) - [ ] Coordinate release with martin-estate#56 deploy + new \`@agent-score/pay\` rc tag 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vvillait88
added a commit
to agentscore/python-commerce
that referenced
this pull request
May 5, 2026
…fy+extra_warnings + x402scan discovery (#7) ## Summary Mirrors `agentscore/node-commerce#8` on the Python side. Ships as `1.3.0` (next minor after `1.2.0`). **1. Kill x402 Solana (BREAKING)** - Drop `x402-solana-*` rails. Add `mpp-solana-*` rails (method=`solana`). - `validate_x402_network_config` now base-only (`base_network: str`, was `{base, svm}` object). - `verify_x402_request` takes single `accepted_network: str`. - Rename `X402SolanaConfig` → `SolanaMppConfig`, `X402SolanaRailConfig` → `SolanaMppRailConfig`. - pyproject `x402[evm,fastapi]>=2.8,<3` (no `svm` extra). - **Python parity gap (intentional):** there is no Python equivalent of `@solana/mpp`, so `create_mppx_server` does not ship a Solana rail. Python merchants on Solana implement MPP `solana/charge` server-side themselves. **2. New helpers** - `classify_x402_settle_result` collapses `process_x402_settle` failure phases (`verify_failed` / `settle_failed` / `facilitator_error` / `no_requirements`) to `(status, code, message, next_steps)`. Mapping is identical to the node side: 400 / 503 / 503 / 500. - `extra_warnings` field on `BuildAgentInstructionsInput`; append per-order warnings on top of defaults. - Solana-network rejection branch in `verify_x402_request`: clients presenting an x402 credential on a `solana:*` network get a behavior-only hint pointing at the `solana/charge` rail. - `process_x402_settle` wraps facilitator throws as `phase='facilitator_error'` across all steps. **3. x402scan discovery (Layer 1)** - `build_well_known_x402` builder. - OpenAPI extension helpers (`siwx_security_scheme`, `x_payment_info_extension`, `x_guidance_extension`). **4. Internal-disclosure cleanup** - Replaced threat-model rationale in source docstrings with behavior-only language (`stripe_multichain/pi_cache.py`, `identity/types.py`, `identity/signer.py`). - Genericized Martin Estate / martinestate.com / martin-estate-wine-commerce / "wine-purchase" baked into examples + tests + docstrings. - Removed `pending_identity` internal state name from agent-visible warnings. - Removed `x402[evm,svm]` references from CLAUDE.md (the upstream extra is gone; we depend on `x402[evm]`). ## Test plan - [x] 715 pytest tests passing (3 skipped) - [x] ruff + ty all green - [x] CodeQL + dependency + python audit scans green ## Downstream impact Once this lands and `agentscore-commerce==1.3.0` publishes: - `agentscore/core#230` will pass CI (currently red on `agentscore-commerce[mppx,x402]>=1.3.0` resolution). - The store route's `verify_x402_request` + `process_x402_settle` + `classify_x402_settle_result` wiring depends on this SDK release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <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
Major release pulling together everything that landed on this branch beyond the original facilitator-error fix. Ships as
1.3.0(next minor after1.2.0).1. Kill x402 Solana, add MPP
solana/charge(BREAKING)x402-solana-mainnet/x402-solana-devnetrails from the registry.validateX402NetworkConfig(now base-only) andverifyX402Request(singleacceptedNetwork: string, was{base, svm}object).createX402Serverrail union; drop the SVM extraction branch inextractPaymentSigner.mpp-solana-mainnet/mpp-solana-devnetrails (method=solana).solanarail tocreateMppxServer, wiring@solana/mpp/servercharge.@x402/svmfrom devDependencies; add@solana/mpp+@solana/kitas optional peer deps.x402_solana→solana_mppfield across challenge builders + discovery surfaces.2. New helpers
classifyX402SettleResultcollapsesprocessX402Settlefailure phases (verify_failed/settle_failed/facilitator_error/no_requirements) to(status, code, message, next_steps). Maps to 400 / 503 / 503 / 500.extraWarningsfield onbuildAgentInstructions— append per-order warnings on top of the SDK's default protocol-footgun set.verifyX402Request: clients presenting an x402 credential on asolana:*network get a behavior-only hint pointing at thesolana/chargerail. Case-insensitive prefix match.verifyX402Requestnow wraps facilitator throws asphase: 'facilitator_error'cleanly acrossbuild_requirements,enrich_extensions, andprocess_payment_requeststeps.3. Solana signer recovery in
extractPaymentSignerdid:pkh:eip155:<chain>:<addr>source, (b) Solana DIDdid:pkh:solana:<genesis>:<addr>source, (c) pull-mode tx decode that reads the SPLTransferCheckedauthority via@solana/kit(optional peer).@solana/mpp/clientdoesn't setCredential.source, which silently weakened wallet-auth security relative to EVM. Now the gate'sverifyWalletSignerMatchenforces same-operator on Solana the same way it does on EVM.payload.type === 'transaction') covered. Push mode (payload.type === 'signature') returns null (RPC fetch deferred; pay uses pull mode by default).4. x402scan discovery (Layer 1)
buildWellKnownX402builder for the/.well-known/x402endpoint.siwxSecurityScheme,xPaymentInfoExtension,xGuidanceExtension).5. Internal-disclosure cleanup
core.ts,stripe-multichain/pi-cache.ts,payment/x402_validation.ts,signer-match.test.ts.pending_identityfrom agent-visible warnings; agents see "the order will not complete."Test plan
Downstream impact
Once this lands and
@agent-score/commerce@1.3.0publishes:agentscore/martin-estate#56will pass CI (currently red onacceptedNetwork,extraWarnings,solana_mppRailKey).agentscore/pay#17consumes the new SDK shape via the rails registry.🤖 Generated with Claude Code