x402 Error Handling

Table of contents

  1. 402 Payment Required
    1. Initial 402 (Normal)
    2. Payment Rejected 402
    3. Common Payment Errors
  2. 400 Bad Request
    1. Missing Content-Length
    2. Invalid TTL
    3. Invalid Path
  3. 413 Payload Too Large
  4. 500 Internal Server Error
  5. 503 Service Unavailable
  6. Handling Errors
    1. JavaScript Example
    2. Retry Logic
  7. Debugging Tips
    1. Check Payment Header
    2. Verify Network
    3. Test Without Payment
    4. Check Wallet
  8. Support

402 Payment Required

Initial 402 (Normal)

This is the expected response when no payment is provided:

{
  "x402Version": 1,
  "accepts": [{
    "scheme": "exact",
    "network": "eip155:324705682",
    "maxAmountRequired": "10000",
    "payTo": "0x..."
  }],
  "error": "Payment Required"
}

This is NOT an error - it’s how you get payment requirements.

Payment Rejected 402

When your payment is invalid:

{
  "error": "PAYMENT_INVALID",
  "message": "Invalid payment signature"
}

Or:

{
  "error": "PAYMENT_VERIFICATION_FAILED",
  "message": "Facilitator verify failed: 400 Invalid signature"
}

Common Payment Errors

Error Cause Solution
Invalid signature Wrong signing method Use EIP-712 typed data
Insufficient balance Not enough USDC Add USDC to wallet
Nonce already used Replay attack Generate new nonce
Payment expired Took too long Create fresh payment
Wrong network Different chain Use SKALE Europa
Amount too low Underpayment Pay full amount

400 Bad Request

Missing Content-Length

{
  "error": "BAD_REQUEST",
  "message": "Content-Length header required"
}

Solution: Always include Content-Length header.

Invalid TTL

{
  "error": "BAD_REQUEST",
  "message": "Invalid X-Fula-TTL value"
}

Solution: TTL must be between 60 and 2592000.

Invalid Path

{
  "error": "BAD_REQUEST",
  "message": "Invalid bucket or key"
}

Solution: Use valid bucket/key (alphanumeric, hyphens, underscores).


413 Payload Too Large

{
  "error": "PAYLOAD_TOO_LARGE",
  "message": "Request body too large"
}

Solution: Reduce file size or use chunked upload.


500 Internal Server Error

{
  "error": "INTERNAL_ERROR",
  "message": "An unexpected error occurred"
}

Action: Retry after a few seconds. Contact support if persistent.


503 Service Unavailable

{
  "error": "SERVICE_UNAVAILABLE",
  "message": "Service temporarily unavailable"
}

Action: Wait and retry with exponential backoff.


Handling Errors

JavaScript Example

async function uploadWithPayment(url, content, payment) {
  const response = await fetch(url, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/octet-stream',
      'Content-Length': content.length.toString(),
      'X-PAYMENT': payment,
    },
    body: content,
  });

  if (response.status === 402) {
    const error = await response.json();
    if (error.error === 'PAYMENT_INVALID') {
      throw new PaymentError('Payment rejected', error);
    }
    // Initial 402 - need to pay
    return { needsPayment: true, requirements: error };
  }

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || `HTTP ${response.status}`);
  }

  return response.json();
}

Retry Logic

async function uploadWithRetry(url, content, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      // Get requirements
      const req1 = await fetch(url, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/octet-stream',
          'Content-Length': content.length.toString(),
        },
        body: content,
      });

      if (req1.status !== 402) {
        throw new Error('Expected 402');
      }

      const requirements = await req1.json();

      // Sign payment
      const payment = await signPayment(requirements);

      // Upload with payment
      const req2 = await fetch(url, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/octet-stream',
          'Content-Length': content.length.toString(),
          'X-PAYMENT': payment,
        },
        body: content,
      });

      if (req2.ok) {
        return req2.json();
      }

      if (req2.status >= 500) {
        // Retry on server errors
        await sleep(Math.pow(2, attempt) * 1000);
        continue;
      }

      // Client error - don't retry
      throw new Error(`Upload failed: ${req2.status}`);

    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
    }
  }
}

Debugging Tips

Check Payment Header

# Decode X-PAYMENT to inspect
echo "YOUR_PAYMENT_HEADER" | base64 -d | jq .

Verify Network

curl https://x402.api.cloud.fx.land/health/pricing | jq .network
# Should be "eip155:324705682"

Test Without Payment

curl -v -X PUT "https://x402.api.cloud.fx.land/test/debug.txt" \
  -H "Content-Type: text/plain" \
  -H "Content-Length: 4" \
  -d "test"
# Should return 402 with requirements

Check Wallet

Ensure USDC balance on SKALE Europa before attempting payment.


Support

For persistent errors:

  1. Note the error message and code
  2. Record the timestamp
  3. Include request details (without signatures)
  4. Open issue on GitHub