feat(payments): add MPP (Machine Payments Protocol) support - #620
feat(payments): add MPP (Machine Payments Protocol) support#620rajuans wants to merge 2 commits into
Conversation
Add MPP alongside the existing x402 protocol in ProcessPayment. MPP servers
answer 402 with WWW-Authenticate: Payment challenges; the SDK selects the one
challenge the payment instrument can satisfy and mints an Authorization header.
Core:
- mpp.py: parse WWW-Authenticate challenges and select one (charge-intent,
unexpired, method the instrument satisfies: evm/tempo->ETHEREUM, solana->SOLANA;
ordered by NETWORK_PREFERENCES). The selected challenge is forwarded verbatim so
the challenge HMAC stays valid.
- generate_payment_header auto-detects protocol from the 402 and returns
{"Authorization": "Payment <token>"} for MPP; x402 path unchanged.
- Optional buyer_pays_gas_fees passthrough (MPP buyerPaysGasFees).
- _model_patch.py: inject MPP shapes into the loaded botocore model when the
installed release predates them; idempotent, no-op once shipped natively,
never mutates installed botocore.
Integrations:
- Strands plugin and LangGraph middleware detect MPP 402s (WWW-Authenticate)
and settle them; buyer_pays_gas_fees exposed on the shared config.
Tests: unit coverage for parsing, selection, model patch, manager routing, and
both integrations; integ tests gated on TEST_* env vars.
Verified against the live mpp.dev 402 and prod ProcessPayment (request shape
accepted; end-to-end settlement pending account MPP entitlement).
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #620 +/- ##
=======================================
Coverage ? 88.66%
=======================================
Files ? 115
Lines ? 9813
Branches ? 1504
=======================================
Hits ? 8701
Misses ? 739
Partials ? 373
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| # leaves the protocol default in place rather than asserting a choice the | ||
| # caller did not make. | ||
| if buyer_pays_gas_fees is not None: | ||
| mpp_input["buyerPaysGasFees"] = bool(buyer_pays_gas_fees) |
There was a problem hiding this comment.
bool() treats every non-empty string as true, so buyer_pays_gas_fees="false" sends buyerPaysGasFees: true and authorizes additional wallet charges. Please require None or an actual bool and forward it unchanged, matching the strict integration-config validation.
There was a problem hiding this comment.
Fixed in 41da888 — good catch, this was a real money-moving bug.
bool() is gone. The value must now be None or an actual bool and is forwarded unchanged; anything else raises PaymentError rather than being coerced. This matches the strict integration-config validation as you suggested.
Added a regression test for the exact case you named (buyer_pays_gas_fees="false" must never authorize charges), plus a parametrized test covering "false", "true", "no", 1, 0, [], {} — all rejected, and process_payment is never called.
| if is_mpp_payment_required(payment_required_request): | ||
| logger.debug("Detected MPP payment required request") | ||
| return self._generate_mpp_payment_header( | ||
| payment_instrument_id=payment_instrument_id, | ||
| payment_session_id=payment_session_id, | ||
| payment_required_request=payment_required_request, | ||
| user_id=user_id, | ||
| network_preferences=network_preferences, | ||
| client_token=client_token, | ||
| buyer_pays_gas_fees=buyer_pays_gas_fees, | ||
| ) |
There was a problem hiding this comment.
Inline MPP/x402 endpoints intentionally advertise both protocols. This unconditional branch chooses MPP whenever any Payment challenge exists, then fails if that challenge does not match the instrument without inspecting a satisfiable x402 option. Should we fall back to x402 when MPP selection is unsatisfiable and an x402 requirement is present?
There was a problem hiding this comment.
Agreed, and fixed in 41da888 — MPP now falls back to x402 when no advertised challenge is satisfiable and the same 402 carries a usable x402 requirement.
Behavior:
| 402 contents | Outcome |
|---|---|
| Satisfiable MPP challenge (± x402) | MPP → Authorization |
Unsatisfiable MPP + usable x402 accepts |
Falls back to x402 → X-PAYMENT |
| Unsatisfiable MPP, no usable x402 | PaymentError |
One deliberate constraint worth flagging: the fallback catches only MppChallengeSelectionError, which is raised before anything is submitted. A failure after the payment reaches the service (insufficient budget, malformed output, verification failure) still propagates — falling back at that point could charge the buyer twice. There is a test asserting exactly one process_payment call in that case.
To make this work, _generate_mpp_payment_header now lets MppChallengeSelectionError propagate instead of converting it to PaymentError, so the dispatcher can tell a selection failure apart from a submission failure. Callers still see a PaymentError when no fallback exists.
_has_x402_payment_required is read-only and non-raising, so a malformed x402 payload reports "no fallback available" rather than masking the original MPP error. Empty accepts, missing x402Version, and absent body are all treated as no-fallback.
| MPP_SOLANA_NETWORK_ALIASES = { | ||
| "mainnet": "solana-mainnet", | ||
| "devnet": "solana-devnet", | ||
| "localnet": "solana-testnet", |
There was a problem hiding this comment.
Solana MPP uses localnet as a distinct local RPC/Surfpool environment, not Solana testnet. This alias lets a local-only challenge satisfy a solana-testnet preference and beat a payable devnet challenge. Should we keep localnet distinct or unranked unless explicitly supported?
There was a problem hiding this comment.
yes, it should be just devnet/testnet.
There was a problem hiding this comment.
Fixed in 41da888 — the localnet alias is removed, so the map is now mainnet/devnet/testnet only (matching @rajuans' confirmation).
localnet is now left unranked rather than misranked: challenge_network() returns None, so it sorts last on preference order but stays selectable when it is the only advertised option. That keeps a local Surfpool setup usable for development without ever letting it outrank a payable public-network challenge.
Two tests cover the regression you identified:
- a
localnet+devnetpair must selectdevnet localnetalone is still selectable
Three fixes from PR review, plus Strands plugin MPP integration tests. 1. buyer_pays_gas_fees is no longer coerced with bool(). Every non-empty string is truthy, so buyer_pays_gas_fees="false" sent buyerPaysGasFees: true and authorized additional wallet charges. Now requires None or an actual bool and forwards it unchanged, matching the integration-config validation. 2. Fall back to x402 when no advertised MPP challenge is satisfiable and the same 402 carries a usable x402 requirement. Endpoints that advertise both protocols previously failed outright whenever the MPP challenge did not match the instrument, without considering a payable x402 option. The fallback is limited to selection failures (pre-submission); a failure after the payment was submitted still propagates, so a buyer can never be charged twice. 3. Drop the localnet -> solana-testnet alias. localnet is a distinct local RPC/Surfpool environment, and the alias let a local-only challenge satisfy a solana-testnet preference and outrank a payable devnet challenge. Unmapped networks are now unranked rather than misranked: still selectable when they are the only option, but never winning on preference order. Also adds MPP coverage to tests_integ Strands plugin tests: live mpp.dev challenge detection, verification that http_request preserves the WWW-Authenticate header, and hook-flow tests for signing, post-payment rejection, selection failure, and independent x402/MPP state.
Add MPP alongside the existing x402 protocol in ProcessPayment. MPP servers answer 402 with WWW-Authenticate: Payment challenges; the SDK selects the one challenge the payment instrument can satisfy and mints an Authorization header.
Core:
Integrations:
Tests: unit coverage for parsing, selection, model patch, manager routing, and both integrations; integ tests gated on TEST_* env vars.
Verified against the live mpp.dev 402 and prod ProcessPayment (request shape accepted; end-to-end settlement pending account MPP entitlement).
Issue #, if available:
Description of changes:
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.