What happened?
At commit e1ea56d, the roles that verify a mandate chain do not agree on what to do when the agent-provider public key cannot be loaded from AGENT_PROVIDER_PUB_PATH. Two refuse; the settlement component continues.
Roles that refuse:
| File |
Line |
Returned |
merchant_agent_mcp/server.py |
663 |
no_public_key (in create_checkout) |
merchant_agent_mcp/server.py |
866 |
agent_provider_key_missing (in complete_checkout) |
merchant_payment_processor_mcp/server.py |
242 |
agent_provider_key_missing |
The role that does not:
In x402_psp_mcp/server.py, agent_provider_pub is initialised to None at line 110 and loaded only if the path exists. A missing file, or an OSError, ValueError or JSONDecodeError while reading it, leaves it None and logs that SD-JWT verification is being skipped (lines 118 and 122).
The verification step is then guarded at line 125 by if agent_provider_pub:. That block contains MandateClient().verify(), PaymentMandateChain.parse() and parsed_chain.verify(). There is no else branch returning an error, and no other error return in the file for this condition.
Two things follow from the structure. The signature check and the constraint evaluation are skipped together, since parsed_chain.verify() is the call that evaluates the Payment Mandate constraints and is inside the same guard. And execution continues past the guard to the binding check, ecrecover, routing, and the optional on-chain broadcast controlled by BROADCAST_ON_CHAIN.
Specification context. AP2 v0.2, in docs/ap2/specification.md at the same commit, states under Verification that the Merchant Payment Processor must verify that the Payment Credential is appropriately scoped to the Checkout. In this component the verification that would establish that scoping is conditional on the key being available.
Steps to reproduce
This is a source observation. I did not reproduce an unverified settlement.
- Open
code/samples/python/src/roles/x402_psp_mcp/server.py at commit e1ea56d.
- Lines 110–122:
agent_provider_pub starts as None; on a missing or unreadable key file the code logs that verification is skipped and leaves it None.
- Line 125: the whole verification block is inside
if agent_provider_pub:, with no else.
- Continue past the block: binding,
ecrecover, routing, optional broadcast.
- Compare with lines 663 and 866 of
merchant_agent_mcp/server.py and line 242 of merchant_payment_processor_mcp/server.py, which return an error under the corresponding condition.
What I expected: the same missing-key condition handled the same way across roles that all need the key for the same purpose, or a comment explaining why the settlement component differs.
What I have not established. Whether the branch is reachable in a provisioned deployment: in the sample scenario the key is written before the flow starts, and when I ran the x402 flow end to end with the key present, verification did occur. Whether the merchant and the PSP can encounter different key availability in a supported deployment: the path only reaches the PSP after the merchant's own verification succeeded, so the two branches are not shown firing in a single run. Whether an unverified settlement can be driven to a successful broadcast: not tested.
Question: is the asymmetry intentional? If the sample PSP is meant to tolerate a missing verification key for local demonstration, saying so in a comment would make the difference from the other two roles legible.
Prior report. I sent this to the Google security team first, since the affected path can reach a broadcast. They replied that it does not meet their threshold for tracking as a security bug and invited me to file it publicly, which is what this is. Reported there as issue 540649426, closed as Infeasible on 2026-07-30.
Relation to #308. Separate observation in the same sample flow. #308 concerns ordering — internal state persisted before settlement is confirmed. This one concerns whether verification runs at all when a configuration file is absent. They share a file only incidentally.
Relevant log output
Code of Conduct
What happened?
At commit
e1ea56d, the roles that verify a mandate chain do not agree on what to do when the agent-provider public key cannot be loaded fromAGENT_PROVIDER_PUB_PATH. Two refuse; the settlement component continues.Roles that refuse:
merchant_agent_mcp/server.pyno_public_key(increate_checkout)merchant_agent_mcp/server.pyagent_provider_key_missing(incomplete_checkout)merchant_payment_processor_mcp/server.pyagent_provider_key_missingThe role that does not:
In
x402_psp_mcp/server.py,agent_provider_pubis initialised toNoneat line 110 and loaded only if the path exists. A missing file, or anOSError,ValueErrororJSONDecodeErrorwhile reading it, leaves itNoneand logs that SD-JWT verification is being skipped (lines 118 and 122).The verification step is then guarded at line 125 by
if agent_provider_pub:. That block containsMandateClient().verify(),PaymentMandateChain.parse()andparsed_chain.verify(). There is noelsebranch returning an error, and no other error return in the file for this condition.Two things follow from the structure. The signature check and the constraint evaluation are skipped together, since
parsed_chain.verify()is the call that evaluates the Payment Mandate constraints and is inside the same guard. And execution continues past the guard to the binding check,ecrecover, routing, and the optional on-chain broadcast controlled byBROADCAST_ON_CHAIN.Specification context. AP2 v0.2, in
docs/ap2/specification.mdat the same commit, states under Verification that the Merchant Payment Processor must verify that the Payment Credential is appropriately scoped to the Checkout. In this component the verification that would establish that scoping is conditional on the key being available.Steps to reproduce
This is a source observation. I did not reproduce an unverified settlement.
code/samples/python/src/roles/x402_psp_mcp/server.pyat commite1ea56d.agent_provider_pubstarts asNone; on a missing or unreadable key file the code logs that verification is skipped and leaves itNone.if agent_provider_pub:, with noelse.ecrecover, routing, optional broadcast.merchant_agent_mcp/server.pyand line 242 ofmerchant_payment_processor_mcp/server.py, which return an error under the corresponding condition.What I expected: the same missing-key condition handled the same way across roles that all need the key for the same purpose, or a comment explaining why the settlement component differs.
What I have not established. Whether the branch is reachable in a provisioned deployment: in the sample scenario the key is written before the flow starts, and when I ran the x402 flow end to end with the key present, verification did occur. Whether the merchant and the PSP can encounter different key availability in a supported deployment: the path only reaches the PSP after the merchant's own verification succeeded, so the two branches are not shown firing in a single run. Whether an unverified settlement can be driven to a successful broadcast: not tested.
Question: is the asymmetry intentional? If the sample PSP is meant to tolerate a missing verification key for local demonstration, saying so in a comment would make the difference from the other two roles legible.
Prior report. I sent this to the Google security team first, since the affected path can reach a broadcast. They replied that it does not meet their threshold for tracking as a security bug and invited me to file it publicly, which is what this is. Reported there as issue 540649426, closed as Infeasible on 2026-07-30.
Relation to #308. Separate observation in the same sample flow. #308 concerns ordering — internal state persisted before settlement is confirmed. This one concerns whether verification runs at all when a configuration file is absent. They share a file only incidentally.
Relevant log output
Code of Conduct