fix: increase x402 payment timeout to 5min and add 402 re-sign logic

AI inference (especially DeepSeek) often exceeds the default 120s HTTP
timeout, causing the client to disconnect while the server completes
successfully — resulting in repeated payments on each retry.

Changes:
- Set X402Timeout = 5min for all x402 providers (Claw402, BlockRunBase, BlockRunSol)
- Handle 402 during payment retry by re-extracting Payment-Required
  header and re-signing instead of failing immediately
- Increase payment retry attempts from 3 to 5 for unstable gateways
This commit is contained in:
tinkle-community
2026-03-12 14:06:28 +08:00
parent fcda921d41
commit b5061d1b8f
4 changed files with 42 additions and 9 deletions
+1
View File
@@ -72,6 +72,7 @@ func NewBlockRunBaseClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
mcp.WithProvider(mcp.ProviderBlockRunBase),
mcp.WithModel(DefaultBlockRunModel),
mcp.WithBaseURL(DefaultBlockRunBaseURL),
mcp.WithTimeout(X402Timeout),
}
allOpts := append(baseOpts, opts...)
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
+1
View File
@@ -52,6 +52,7 @@ func NewBlockRunSolClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
mcp.WithProvider(mcp.ProviderBlockRunSol),
mcp.WithModel(DefaultBlockRunModel),
mcp.WithBaseURL(DefaultBlockRunSolURL),
mcp.WithTimeout(X402Timeout),
}
allOpts := append(baseOpts, opts...)
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
+1
View File
@@ -69,6 +69,7 @@ func NewClaw402ClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
mcp.WithProvider(mcp.ProviderClaw402),
mcp.WithModel(DefaultClaw402Model),
mcp.WithBaseURL(DefaultClaw402URL),
mcp.WithTimeout(X402Timeout),
}
allOpts := append(baseOpts, opts...)
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
+39 -9
View File
@@ -16,12 +16,17 @@ import (
)
const (
// X402MaxPaymentRetries is the number of retries for 5xx errors on the
// payment-signed request. The same payment signature is reused (no double-charge).
X402MaxPaymentRetries = 3
// X402MaxPaymentRetries is the number of retries for 5xx/expired-402 errors
// on the payment-signed request. Payment is re-signed on 402 (no double-charge).
X402MaxPaymentRetries = 5
// X402RetryBaseWait is the base wait between payment retry attempts.
X402RetryBaseWait = 3 * time.Second
// X402Timeout is the HTTP timeout for x402 payment providers.
// AI inference (especially DeepSeek) can take several minutes; the default
// 120s causes premature timeouts that trigger duplicate payments.
X402Timeout = 5 * time.Minute
)
// ── Shared x402 types ────────────────────────────────────────────────────────
@@ -131,7 +136,7 @@ func DoX402Request(
return nil, fmt.Errorf("failed to sign x402 payment: %w", err)
}
// Retry loop for 5xx errors on the payment-signed request.
// Retry loop for 5xx / expired-402 errors on the payment-signed request.
var lastBody []byte
var lastStatus int
for attempt := 1; attempt <= X402MaxPaymentRetries; attempt++ {
@@ -173,16 +178,41 @@ func DoX402Request(
lastBody = body2
lastStatus = resp2.StatusCode
// Retry on 5xx server errors
if resp2.StatusCode >= 500 && attempt < X402MaxPaymentRetries {
retryable := resp2.StatusCode >= 500 || resp2.StatusCode == http.StatusPaymentRequired
if retryable && attempt < X402MaxPaymentRetries {
wait := X402RetryBaseWait * time.Duration(attempt)
logger.Warnf("⚠️ [%s] Server error (status %d), retrying in %v (%d/%d)...",
providerTag, resp2.StatusCode, wait, attempt+1, X402MaxPaymentRetries)
// If we got 402 again, the payment signature expired — re-sign.
if resp2.StatusCode == http.StatusPaymentRequired {
newHeader := resp2.Header.Get("Payment-Required")
if newHeader == "" {
newHeader = resp2.Header.Get("X-Payment-Required")
}
if newHeader != "" {
newSig, signErr := signFn(newHeader)
if signErr == nil {
paymentSig = newSig
logger.Warnf("⚠️ [%s] Payment expired (402), re-signed and retrying in %v (%d/%d)...",
providerTag, wait, attempt+1, X402MaxPaymentRetries)
} else {
logger.Warnf("⚠️ [%s] Payment expired (402), re-sign failed: %v, retrying in %v (%d/%d)...",
providerTag, signErr, wait, attempt+1, X402MaxPaymentRetries)
}
} else {
logger.Warnf("⚠️ [%s] Got 402 but no new Payment-Required header, retrying in %v (%d/%d)...",
providerTag, wait, attempt+1, X402MaxPaymentRetries)
}
} else {
logger.Warnf("⚠️ [%s] Server error (status %d), retrying in %v (%d/%d)...",
providerTag, resp2.StatusCode, wait, attempt+1, X402MaxPaymentRetries)
}
time.Sleep(wait)
continue
}
// Non-5xx error or final attempt — fail
// Non-retryable error or final attempt — fail
break
}