Skip to content

Intermittent "unable to estimate gas" errors on /settle endpoint - Base Mainnet USDC payments #1065

@cybertheory

Description

@cybertheory

Summary

We're experiencing intermittent unable to estimate gas errors when calling the CDP x402 Facilitator /settle endpoint on Base mainnet. The failures are non-deterministic - identical requests sometimes succeed and sometimes fail, even with fresh signatures and nonces each time.

Environment

  • Network: Base Mainnet (chainId: 8453)
  • Asset: USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
  • Scheme: exact (ERC-3009 TransferWithAuthorization)
  • Payment Amount: 1000 atomic units (0.001 USDC)
  • CDP SDK Version: Latest (@coinbase/cdp-sdk)

Error Details

{
  "error": "facilitator_settle_failed",
  "debug": {
    "error": "Facilitator settle failed (400): {\"errorMessage\": \"failed to send transaction: error (status 400): invalid_request: unable to estimate gas\", \"errorReason\": \"invalid_payload\", \"network\": \"base\", \"payer\": \"0x9A95677Df6ED534bbb2521936eEca92B268B94Db\", \"success\": false}"
  }
}

Reproduction Pattern

Running 5 identical API requests with 3-second delays between each:

Test # Result
1 FAILED
2 SUCCESS
3 FAILED
4 FAILED
5 SUCCESS

Success rate: ~40% with identical code and configuration.

What We've Verified

Fresh Nonce Each Request

Each request generates a cryptographically random 32-byte nonce:

TypeScript:

const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
const nonce = '0x' + Array.from(randomBytes)
  .map(b => b.toString(16).padStart(2, '0'))
  .join('');

Python:

import secrets
nonce = '0x' + secrets.token_hex(32)

Confirmed via logs - each request has a unique nonce:

Request 1: 0xd56eed5c3460dd37ded5a77fd228142997c2613f...
Request 2: 0x691c9ed5ab4cc628de537fcce4bddb81f3cf8c9e...
Request 3: 0xef7d9741968707e660ed9f0c89d00d0a98935c5b...

Correct EIP-712 Domain

Using the official USDC contract domain parameters:

const domain = {
  name: "USD Coin",
  version: "2",
  chainId: 8453,
  verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
};

Sufficient Balance

  • USDC Balance: ~2.99 USDC (verified via balanceOf)
  • ETH Balance: Sufficient for any fees
  • Each payment: 0.001 USDC

/verify Passes

The /verify endpoint returns isValid: true before /settle fails:

{
  "isValid": true,
  "invalidReason": null
}

On-Chain Transactions Succeed When They Work

Successful payments are confirmed on-chain. Balance decreases correctly. The USDC transfers complete to the payTo address.

Code Examples (From our own SDK implementation for https://waterfall.finance)

TypeScript SDK Usage

import { WaterfallClient } from 'waterfall-ts';

const client = new WaterfallClient({ apiKey: 'wf_xxx' });
await client.configureWallet({ walletName: 'My Wallet' });

// This works ~40% of the time
const response = await client.chat.send({
  model: 'openai/gpt-3.5-turbo',
  messages: [{ role: 'user', content: 'Hello' }]
});

// Multi-turn conversations fail on subsequent calls
const response2 = await client.chat.send({
  model: 'openai/gpt-3.5-turbo',
  messages: [
    { role: 'user', content: 'Hello' },
    { role: 'assistant', content: response.choices[0].message.content },
    { role: 'user', content: 'Follow up question' }
  ]
});

Python SDK Usage

from waterfall import WaterfallClient

client = WaterfallClient(api_key='wf_xxx')
client.configure_wallet(wallet_name='My Wallet')

# First call - works ~40% of the time
response1 = client.chat.send(
    model='openai/gpt-3.5-turbo',
    messages=[{'role': 'user', 'content': 'Hello'}]
)

# Second call - often fails with "unable to estimate gas"
response2 = client.chat.send(
    model='openai/gpt-3.5-turbo',
    messages=[
        {'role': 'user', 'content': 'Hello'},
        {'role': 'assistant', 'content': response1.choices[0].message.content},
        {'role': 'user', 'content': 'Follow up'}
    ]
)

x402 Gateway Payment Flow

// 1. Receive 402 Payment Required
const paymentRequired = {
  scheme: 'exact',
  network: 'base',
  asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  payTo: '0x6e560Fd994dA2f434E95Cde3CAA868FB0bbCA8Ba',
  maxAmountRequired: '1000',
  extra: { name: 'USD Coin', version: '2' }
};

// 2. Generate ERC-3009 Authorization
const authorization = {
  from: walletAddress,
  to: paymentRequired.payTo,
  value: paymentRequired.maxAmountRequired,
  validAfter: Math.floor(Date.now() / 1000).toString(),
  validBefore: (Math.floor(Date.now() / 1000) + 600).toString(),
  nonce: generateRandomNonce()
};

// 3. Sign with EIP-712
const signature = await wallet.signTypedData({
  domain: { name: 'USD Coin', version: '2', chainId: 8453, verifyingContract: asset },
  types: { TransferWithAuthorization: [...] },
  primaryType: 'TransferWithAuthorization',
  message: authorization
});

// 4. Call Facilitator
// /verify returns isValid: true
// /settle returns "unable to estimate gas" (intermittent)

Expected Behavior

All valid payment signatures with fresh nonces should settle successfully, especially when:

  • /verify returns isValid: true
  • Wallet has sufficient USDC balance
  • EIP-712 domain matches the token contract
  • Nonce has never been used before

Actual Behavior

~60% of valid, verified payments fail at /settle with unable to estimate gas, despite:

  • Passing verification
  • Using fresh nonces
  • Having sufficient balance
  • Using correct domain parameters

Questions

  1. Is there internal rate limiting on the facilitator that could cause this pattern?
  2. Are there multiple facilitator instances, some of which may be unhealthy?
  3. Is there a recommended retry strategy for handling these intermittent failures?
  4. Could there be RPC node issues on the facilitator's side affecting gas estimation?

Workaround Attempted

Adding delays (2s, 10s, 30s) between requests does not resolve the issue. The failures appear random regardless of timing.

Impact

This makes multi-turn conversations unreliable for production use, as subsequent API calls in a session frequently fail even though the payment signatures are valid.

Additional Context

  • First request in a session has the same ~40% failure rate as subsequent requests
  • Failures are not correlated with time of day
  • Same behavior observed across TypeScript and Python SDKs
  • On-chain transaction history shows successful payments when /settle succeeds

Metadata

Metadata

Assignees

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