@@ -163,11 +163,6 @@ def after_tool_call(self, event: AfterToolCallEvent) -> None:
163163 )
164164 return
165165
166- # Check if payment retry limit has been reached
167- if self ._check_payment_retry_limit (event ):
168- logger .warning ("Payment processing retry limit has been reached. Processing skipped." )
169- return
170-
171166 # Check if response is a 402 Payment Required
172167 if not hasattr (event , "result" ) or event .result is None :
173168 return
@@ -192,9 +187,6 @@ def after_tool_call(self, event: AfterToolCallEvent) -> None:
192187
193188 logger .info ("Detected 402 Payment Required response from tool: %s" , event .tool_use .get ("name" , "unknown" ))
194189
195- # Increment retry count in invocation state
196- self ._increment_payment_retry_count (event )
197-
198190 # Build payment_required_request dict using handler methods
199191 headers = handler .extract_headers (event .result )
200192 body = handler .extract_body (event .result )
@@ -204,18 +196,24 @@ def after_tool_call(self, event: AfterToolCallEvent) -> None:
204196 "body" : body or {},
205197 }
206198
207- # If we already retried with payment credentials and still got a 402,
208- # this is a post-payment failure (e.g., insufficient balance, invalid signature ).
209- # Propagate as an interrupt instead of retrying again to avoid infinite loops .
210- if self ._is_post_payment_failure (event , body ):
199+ # If we previously signed successfully and still got a 402, the server
200+ # rejected the payment for a non-retryable reason (e.g., insufficient balance).
201+ # Stop processing — the existing 402 result is already structured for the LLM .
202+ if self ._has_successful_signing (event ):
211203 logger .warning (
212- "Received 402 after payment retry for tool %s — treating as payment failure" ,
204+ "Received 402 after successful signing for tool %s — post- payment failure, not retrying " ,
213205 event .tool_use .get ("name" , "unknown" ),
214206 )
215- error_msg = self ._extract_payment_error_message (body )
216- self ._store_payment_failure_state (event , PaymentError (f"Payment failed after retry: { error_msg } " ))
217207 return
218208
209+ # Check if signing retry limit has been reached
210+ if self ._check_payment_retry_limit (event ):
211+ logger .warning ("Payment signing retry limit reached. Processing skipped." )
212+ return
213+
214+ # Increment retry count for signing attempts
215+ self ._increment_payment_retry_count (event )
216+
219217 # Validate tool input before processing payment
220218 tool_input = event .tool_use .get ("input" , {})
221219 if not handler .validate_tool_input (tool_input ):
@@ -232,10 +230,11 @@ def after_tool_call(self, event: AfterToolCallEvent) -> None:
232230 self ._store_payment_failure_state (event , Exception ("Failed to apply payment header" ))
233231 return
234232
233+ # Mark that signing succeeded for this tool use — if we get another 402
234+ # after this retry, we know it's a server-side rejection, not a signing failure.
235+ self ._mark_successful_signing (event )
236+
235237 # Set retry flag to re-execute the tool with payment credentials.
236- # Do NOT reset the payment retry counter here — it must persist across
237- # retries so that _is_post_payment_failure and _check_payment_retry_limit
238- # can detect repeated 402s and break the loop.
239238 event .retry = True
240239 self ._reset_interrupt_retry_count (event )
241240 logger .info ("Set retry flag to re-execute tool with payment credentials" )
@@ -295,47 +294,32 @@ def _increment_payment_retry_count(self, event: AfterToolCallEvent) -> None:
295294 "Payment retry attempt %d/%d for tool use %s" , retry_count + 1 , self .MAX_PAYMENT_RETRIES , tool_use_id
296295 )
297296
298- def _is_post_payment_failure (self , event : AfterToolCallEvent , body : Optional [Dict [str , Any ]]) -> bool :
299- """Check if this 402 response is a failure after we already retried with payment credentials.
300-
301- A post-payment failure occurs when:
302- 1. We already sent a payment header (retry count > 0 before this increment), AND
303- 2. The 402 response body contains an error that is NOT the initial "payment required"
304- (e.g., "invalid_exact_evm_insufficient_balance", "payment_rejected", etc.)
305-
306- This prevents infinite loops where the plugin keeps signing and retrying
307- against a server that keeps rejecting the payment for non-retryable reasons.
297+ def _has_successful_signing (self , event : AfterToolCallEvent ) -> bool :
298+ """Check if we previously signed a payment successfully for this tool use.
308299
309300 Args:
310301 event: The after tool call event
311- body: The extracted response body (may be None)
312302
313303 Returns:
314- True if this is a post-payment failure that should be propagated as an interrupt
304+ True if signing was previously successful (meaning this 402 is a server-side rejection)
315305 """
316306 tool_use_id = event .tool_use .get ("toolUseId" , "unknown" )
317- payment_retry_key = f"payment_retry_count_{ tool_use_id } "
318- # retry count was already incremented before this check, so > 1 means
319- # we already attempted at least one payment retry
320- retry_count = event .invocation_state .get (payment_retry_key , 0 )
307+ signed_key = f"payment_signed_{ tool_use_id } "
308+ return event .invocation_state .get (signed_key , False )
321309
322- if retry_count <= 1 :
323- return False
310+ def _mark_successful_signing ( self , event : AfterToolCallEvent ) -> None :
311+ """Mark that signing succeeded for this tool use.
324312
325- # If the body contains an error field that is NOT the initial "payment required",
326- # this is a post-payment failure
327- if body and isinstance (body , dict ):
328- error = body .get ("error" , "" )
329- if isinstance (error , str ) and error .lower () not in ("" , "payment required" ):
330- logger .info (
331- "Post-payment failure detected for tool %s: error=%s (retry_count=%d)" ,
332- tool_use_id ,
333- error ,
334- retry_count ,
335- )
336- return True
313+ Called after generate_payment_header and apply_payment_header both succeed,
314+ right before setting event.retry. If a subsequent 402 is received,
315+ _has_successful_signing will return True indicating the failure is server-side.
337316
338- return False
317+ Args:
318+ event: The after tool call event
319+ """
320+ tool_use_id = event .tool_use .get ("toolUseId" , "unknown" )
321+ signed_key = f"payment_signed_{ tool_use_id } "
322+ event .invocation_state [signed_key ] = True
339323
340324 @staticmethod
341325 def _extract_payment_error_message (body : Optional [Dict [str , Any ]]) -> str :
0 commit comments