Skip to content

[Bug] wrapFetchWithPayment fails on consecutive payments with "FiatTokenV2: invalid signature"Β #6

@smartchainark

Description

@smartchainark

[Bug] wrapFetchWithPayment fails on consecutive payments with "FiatTokenV2: invalid signature"

Environment

  • SDK Version: [email protected]
  • Network: Base Sepolia (eip155:84532)
  • USDC Contract: 0x036CbD53842c5426634e7929541eC2318f3dCF7e (FiatTokenV2)
  • Framework: React 19.1.1 + Vite 7.1.7
  • Wallet: MetaMask / In-App Wallet

Description

When using wrapFetchWithPayment to make consecutive API calls with x402 payments, the first payment succeeds, but subsequent payments fail with signature validation errors. This appears to be a caching or state management issue in the client SDK.

Steps to Reproduce

  1. Set up a basic React app with thirdweb x402:
import { createThirdwebClient } from "thirdweb";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { useActiveWallet } from "thirdweb/react";

const client = createThirdwebClient({
  clientId: "your-client-id"
});

// In component
const wallet = useActiveWallet();
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
  1. Make the first payment request:
const response1 = await fetchWithPay('http://localhost:3000/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message: 'Hello' })
});
// βœ… Works fine - Status: 200
  1. Immediately make a second payment request (within 3-5 seconds):
const response2 = await fetchWithPay('http://localhost:3000/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message: 'World' })
});
// ❌ Fails with 402 status and signature error

Expected Behavior

Each payment request should be processed independently with fresh signatures and nonces, allowing consecutive payments without errors.

Actual Behavior

First Request (Success βœ…)

πŸ“€ ε‘ι€ζΆˆζ―οΌŒε‡†ε€‡ζ”―δ»˜...
πŸ”„ ε‘ι€θ―·ζ±‚εˆ°ζœεŠ‘ε™¨...
POST http://localhost:3000/chat 402 (Payment Required)
πŸ“© ζ”Άεˆ°ε“εΊ”οΌŒηŠΆζ€: 200
βœ… ζΆˆζ―ε‘ι€ζˆεŠŸοΌ

Second Request (Failure ❌)

πŸ“€ ε‘ι€ζΆˆζ―οΌŒε‡†ε€‡ζ”―δ»˜...
πŸ”„ ε‘ι€θ―·ζ±‚εˆ°ζœεŠ‘ε™¨...
POST http://localhost:3000/chat 402 (Payment Required)
POST http://localhost:3000/chat 402 (Payment Required)  // Retried
πŸ“© ζ”Άεˆ°ε“εΊ”οΌŒηŠΆζ€: 402
❌ 发送倱θ΄₯: Error: ζ”―δ»˜ε€±θ΄₯: payment_simulation_failed

Server-Side Error

{
  "x402Version": 1,
  "error": "payment_simulation_failed",
  "errorMessage": "Payment simulation failed: {
    \"type\":\"RPC_ERROR\",
    \"chain_id\":84532,
    \"message\":\"execution reverted: FiatTokenV2: invalid signature\",
    \"data\": \"0x08c379a0...\"
  }"
}

Or sometimes:

{
  "x402Version": 1,
  "error": "Settlement error",
  "errorMessage": "Failed to settle payment: TRANSACTION_SIMULATION_FAILED: execution reverted: FiatTokenV2: invalid signature"
}

Backend Configuration

Using waitUntil: "confirmed" for reliable transaction confirmation:

import { facilitator, settlePayment } from "thirdweb/x402";
import { baseSepolia } from "thirdweb/chains";

const thirdwebFacilitator = facilitator({
  client,
  serverWalletAddress: PAYMENT_ADDRESS,
  waitUntil: "confirmed", // Wait for full confirmation
});

const result = await settlePayment({
  paymentData: req.headers["x-payment"],
  resourceUrl: `http://localhost:3000/chat`,
  method: "POST",
  payTo: PAYMENT_ADDRESS,
  network: baseSepolia,
  price: "$0.001",
  facilitator: thirdwebFacilitator,
  routeConfig: {
    description: "AI Chat - Pay per message",
    mimeType: "application/json",
    maxTimeoutSeconds: 60,
  },
});

Analysis

Possible Root Causes

  1. Nonce Caching Issue

    • The SDK may be reusing nonces from previous signatures
    • EIP-3009 TransferWithAuthorization uses random nonces, but there might be collision or caching
  2. Signature Reuse

    • Previous payment signatures might be cached and reused
    • validBefore timestamp might overlap between requests
  3. Payment State Not Reset

    • Internal state in wrapFetchWithPayment or wallet adapter not properly reset after successful payment
  4. Race Condition

    • First transaction not fully confirmed before second payment is initiated
    • Even with waitUntil: "confirmed", client-side state might not be synchronized

Evidence Supporting Cache Issue

  • Time-dependent: Waiting 5-10 seconds between requests works fine
  • First request always works: Fresh state on page load
  • Pattern repeats: Third, fourth requests fail similarly
  • Browser refresh fixes: New page load = fresh state

Temporary Workarounds

1. Add Request Throttling (Client-side)

let lastRequestTime = 0;
const MIN_REQUEST_INTERVAL = 5000; // 5 seconds

const sendMessage = async () => {
  const now = Date.now();
  const timeSinceLastRequest = now - lastRequestTime;
  
  if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) {
    const waitTime = MIN_REQUEST_INTERVAL - timeSinceLastRequest;
    await new Promise(resolve => setTimeout(resolve, waitTime));
  }
  
  lastRequestTime = Date.now();
  // Make request...
};

2. Re-create fetchWithPay Instance

// Don't reuse the same instance
const sendMessage = async () => {
  const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
  const response = await fetchWithPay(url, options);
};

Expected Fix

The SDK should:

  1. Generate fresh nonces for each payment request
  2. Clear signature cache after successful payment
  3. Reset internal state after each wrapFetchWithPayment call completes
  4. Handle concurrent requests properly with queuing or locking mechanism

Additional Context

This issue is blocking production use cases that require:

  • Real-time chat applications with per-message payments
  • High-frequency API calls with micropayments
  • Interactive applications with rapid user actions

Related Issues

  • Similar to permit signature issues in EIP-2612 implementations
  • May be related to FiatTokenV2's strict nonce validation

Environment Details

{
  "dependencies": {
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "thirdweb": "^5.111.8"
  },
  "network": {
    "name": "Base Sepolia",
    "chainId": 84532,
    "usdcContract": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
  }
}

Request for Information

Could the thirdweb team provide guidance on:

  1. Is there internal caching of payment signatures or nonces?
  2. What's the recommended approach for consecutive x402 payments?
  3. Are there any debug flags or logs we can enable to trace this issue?
  4. Is this a known limitation with FiatTokenV2 contracts?

Screenshots/Logs

Available upon request. Can provide:

  • Full browser console logs
  • Network waterfall showing timing
  • Server-side transaction traces
  • Video reproduction

Related Projects:

Thank you for looking into this! x402 is a game-changing protocol, and resolving this issue would enable so many real-time payment use cases. πŸ™

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions