Skip to content

BadNonce errors during rapid sequential payments due to stale nonce fetching #4

@whoabuddy

Description

@whoabuddy

Problem

When making multiple X402 payments in quick succession from the same wallet, transactions fail with BadNonce errors:

broadcast failed: failed to broadcast transaction: broadcast failed: transaction rejected - BadNonce

This happens because @stacks/transactions fetches nonces from an API endpoint that returns stale data.

Root Cause

The underlying issue is in @stacks/transactions - its fetchNonce function uses a stale API endpoint:

// @stacks/transactions/dist/esm/fetch.js (lines 46-52)
async function _getNonceApi({ address, network, client }) {
    const url = `${client.baseUrl}/extended/v1/address/${address}/nonces`;
    const response = await client.fetch(url);
    const result = await response.json();
    return BigInt(result.possible_next_nonce);  // <-- Can return stale data
}

This endpoint (/extended/v1/address/{address}/nonces) is powered by the Stacks indexer which can lag behind confirmed transactions. The correct endpoint is /v2/accounts/{address} which returns real-time nonce data directly from the blockchain.

However, @stacks/transactions accepts an explicit nonce parameter in makeSTXTokenTransfer and makeContractCall - if provided, it skips the fetch entirely. The fix for x402-stacks is to fetch the correct nonce from /v2/accounts and pass it explicitly, bypassing the buggy default behavior.

Evidence:

# Extended API (stale) - shows last_executed_tx_nonce: 4635, possible_next_nonce: 4636
curl "https://api.mainnet.hiro.so/extended/v1/address/SP.../nonces"

# v2/accounts (correct) - shows nonce: 4637
curl "https://api.mainnet.hiro.so/v2/accounts/SP...?proof=0"

# Recent transactions show nonce 4636 already confirmed
curl "https://api.mainnet.hiro.so/extended/v1/address/SP.../transactions?limit=1"
# Returns: { "nonce": 4636, "tx_status": "success" }

Impact

  • Single payments work fine
  • Rapid sequential payments (e.g., test suites, batch operations) fail
  • The problem is intermittent depending on indexer lag

Proposed Fix

The X402PaymentClient should:

  1. Fetch nonce from /v2/accounts instead of relying on @stacks/transactions default behavior
  2. Track nonces locally for rapid sequential payments
  3. Pass explicit nonce to makeSTXTokenTransfer / makeContractCall

Here's a working implementation:

class X402PaymentClient {
    constructor(config) {
        // ... existing code ...

        // Add nonce tracking
        this.nonceCache = new Map();
    }

    /**
     * Get next nonce with local tracking for rapid sequential payments
     */
    async getNextNonce(address, network) {
        const cached = this.nonceCache.get(address);
        const now = Date.now();

        // If we have a recent cached nonce (within 30s), increment and use it
        if (cached && (now - cached.lastUpdated) < 30000) {
            cached.nonce = cached.nonce + BigInt(1);
            cached.lastUpdated = now;
            return cached.nonce;
        }

        // Fetch fresh nonce from v2/accounts (NOT the stale extended API)
        const baseUrl = network instanceof StacksMainnet
            ? 'https://api.mainnet.hiro.so'
            : 'https://api.testnet.hiro.so';
        const url = `${baseUrl}/v2/accounts/${address}?proof=0`;

        const response = await this.httpClient.get(url);
        const fetchedNonce = BigInt(response.data.nonce);

        // Cache for subsequent requests
        this.nonceCache.set(address, {
            nonce: fetchedNonce,
            lastUpdated: now
        });

        return fetchedNonce;
    }

    async signSTXTransfer(details) {
        // ... existing network/address setup ...

        // Fetch correct nonce
        const nonce = details.nonce !== undefined
            ? details.nonce
            : await this.getNextNonce(senderAddress, network);

        const txOptions = {
            // ... existing options ...
            ...(nonce !== undefined && { nonce }),  // Pass explicit nonce
        };

        const transaction = await makeSTXTokenTransfer(txOptions);
        // ...
    }

    // Apply same pattern to signSBTCTransfer and signUSDCxTransfer
}

Workaround

Until this is fixed upstream, users can apply a patch using patch-package. See: https://github.com/whoabuddy/stx402/blob/master/patches/x402-stacks%2B1.1.0.patch

Environment

  • x402-stacks: 1.1.0
  • @stacks/transactions: 7.x
  • Network: mainnet (also affects testnet)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions