@@ -116,32 +116,59 @@ def _header_lookup(headers: dict[str, str], *names: str) -> str | None:
116116 return None
117117
118118
119+ _REGENERATE_WARNING = (
120+ "If you're trying to pay with Tempo USDC, use `tempo request` (sends Authorization: Payment), "
121+ "not a manual X-Payment header. Do NOT use `tempo wallet transfer` — that sends USDC on-chain "
122+ "but will not complete the MPP handshake. For x402 on Base/Solana, use `agentscore-pay pay` so "
123+ "the X-Payment credential is signed and submitted; bare wallet transfers do not complete the handshake."
124+ )
125+
126+
127+ def _regenerate_body (message : str , user_message : str ) -> dict [str , Any ]:
128+ return {
129+ "error" : {"code" : "payment_proof_invalid" , "message" : message },
130+ "next_steps" : {
131+ "action" : "regenerate_payment_credential" ,
132+ "user_message" : user_message ,
133+ "warning" : _REGENERATE_WARNING ,
134+ },
135+ }
136+
137+
119138async def verify_x402_request (input : VerifyX402RequestInput ) -> VerifyX402RequestResult :
120139 """Parse the x402 X-Payment header and validate network + payTo + cache hit.
121140
122141 Returns ``VerifyX402RequestSuccess`` when valid; the caller passes ``payload``
123142 straight into :func:`process_x402_settle`. Returns ``VerifyX402RequestFailure``
124- when invalid — the merchant just returns ``body`` with HTTP ``status``.
143+ when invalid — ``body`` includes ``next_steps`` with ``regenerate_payment_credential``
144+ so agents can recover deterministically from the response alone.
125145
126146 Reads the header from ``payment-signature`` first, falling back to ``x-payment``
127147 (both are in the wild as the binary-friendly transport name evolved).
128148 """
129149 header_value = _header_lookup (input .headers , "payment-signature" , "x-payment" )
130150 if not header_value :
131151 return VerifyX402RequestFailure (
132- body = {"error" : {"code" : "payment_proof_invalid" , "message" : "X-Payment header missing" }},
152+ body = _regenerate_body (
153+ "X-Payment header missing" ,
154+ (
155+ "No X-Payment header was sent. Generate the credential from the 402 "
156+ "challenge and resubmit on the same endpoint."
157+ ),
158+ ),
133159 )
134160
135161 try :
136162 payload = json .loads (base64 .b64decode (header_value ).decode ())
137163 except Exception :
138164 return VerifyX402RequestFailure (
139- body = {
140- "error" : {
141- "code" : "payment_proof_invalid" ,
142- "message" : "X-Payment header is not valid base64 JSON" ,
143- }
144- },
165+ body = _regenerate_body (
166+ "X-Payment header is not valid base64 JSON" ,
167+ (
168+ "The payment credential could not be decoded. Reconstruct the "
169+ "credential from the 402 challenge and retry."
170+ ),
171+ ),
145172 )
146173
147174 accepted = payload .get ("accepted" ) or {}
@@ -153,40 +180,41 @@ async def verify_x402_request(input: VerifyX402RequestInput) -> VerifyX402Reques
153180 input .accepted_svm_network ,
154181 ):
155182 return VerifyX402RequestFailure (
156- body = {
157- "error" : {
158- "code" : "payment_proof_invalid" ,
159- "message" : (
160- f"Unsupported x402 network { signed_network or '<missing>' } ; "
161- f"this server accepts { input .accepted_base_network } (Base) "
162- f"and { input .accepted_svm_network } (Solana)"
163- ),
164- }
165- },
183+ body = _regenerate_body (
184+ (
185+ f"Unsupported x402 network { signed_network or '<missing>' } ; "
186+ f"this server accepts { input .accepted_base_network } (Base) "
187+ f"and { input .accepted_svm_network } (Solana)"
188+ ),
189+ (
190+ "The credential signed for an unsupported network. Pick one of the "
191+ "accepted networks from the 402 challenge and re-sign."
192+ ),
193+ ),
166194 )
167195
168196 is_solana = network_family (signed_network ) == "solana"
169197 re_match = _SOLANA_ADDRESS_RE if is_solana else _EVM_ADDRESS_RE
170198 if not signed_pay_to or not isinstance (signed_pay_to , str ) or not re_match .match (signed_pay_to ):
171199 return VerifyX402RequestFailure (
172- body = {
173- "error" : {
174- "code" : "payment_proof_invalid" ,
175- "message" : (
176- f"Payment payload missing or malformed accepted.payTo address for network { signed_network } "
177- ),
178- }
179- },
200+ body = _regenerate_body (
201+ f"Payment payload missing or malformed accepted.payTo address for network { signed_network } " ,
202+ (
203+ "The credential payload is missing or malformed payTo for the signed "
204+ "network. Reconstruct the credential from the 402 challenge."
205+ ),
206+ ),
180207 )
181208
182209 if not await input .is_cached_address (signed_pay_to ):
183210 return VerifyX402RequestFailure (
184- body = {
185- "error" : {
186- "code" : "payment_proof_invalid" ,
187- "message" : "payTo address not found in cache or expired. Request a fresh 402 challenge and retry." ,
188- }
189- },
211+ body = _regenerate_body (
212+ "payTo address not found in cache or expired. Request a fresh 402 challenge and retry." ,
213+ (
214+ "The deposit address is unknown or expired on this server. Request a "
215+ "fresh 402 challenge and re-sign against the new payTo."
216+ ),
217+ ),
190218 )
191219
192220 return VerifyX402RequestSuccess (
0 commit comments