Granite Upgrade Activates in07d:18h:33m:29s

Security Considerations

Understanding replay attack prevention, time-bounded authorizations, and settlement monitoring in x402.

Overview

The x402 protocol implements multiple layers of security to protect against common attack vectors while maintaining a seamless user experience. These security measures are built on battle-tested Ethereum standards (EIP-712, EIP-3009) and blockchain cryptography.

Replay Attack Prevention

Replay attacks occur when an attacker intercepts a valid payment authorization and attempts to reuse it multiple times. x402 prevents this through three complementary mechanisms:

1. Nonce-Based Protection

Each payment authorization includes a unique random nonce (32-byte hex string):

const nonce = ethers.randomBytes(32);
// Example: 0x3456789012345678901234567890123456789012345678901234567890123456

The USDC contract (following EIP-3009) tracks which nonces have been used on-chain. When a transferWithAuthorization is executed:

  1. The contract checks if the nonce has been used before
  2. If the nonce is new, the transfer proceeds and the nonce is marked as used
  3. If the nonce was already used, the transaction reverts with an error

This makes each authorization single-use. Even if an attacker intercepts the authorization, they cannot reuse it because the nonce is already consumed on-chain.

2. Time-Bounded Authorizations

Every authorization includes validAfter and validBefore timestamps (Unix time in seconds):

{
  "validAfter": "1740672089",   // January 7, 2025, 10:00:00 AM UTC
  "validBefore": "1740672389"   // January 7, 2025, 10:05:00 AM UTC (5 minutes later)
}

The USDC contract enforces these timing constraints:

  • If current time < validAfter: Transaction reverts (not yet valid)
  • If current time ≥ validBefore: Transaction reverts (expired)
  • Only between these times: Transaction can execute

Best practices:

  • Set short validity windows (5-10 minutes) for most payments
  • For delayed/scheduled payments, set appropriate validAfter
  • Never set validBefore too far in the future (increases replay risk if nonce tracking fails)

Example timing strategy:

const now = Math.floor(Date.now() / 1000);
const authorization = {
  validAfter: now.toString(),              // Valid immediately
  validBefore: (now + 300).toString(),     // Expires in 5 minutes
  // ... other fields
};

3. Transaction Hash Uniqueness

Each blockchain transaction has a unique hash. Merchants should track received transaction hashes to ensure they don't fulfill the same payment twice:

// Server-side tracking
const processedTransactions = new Set();

function fulfillPayment(txHash, resource) {
  if (processedTransactions.has(txHash)) {
    throw new Error("Payment already fulfilled");
  }

  // Deliver resource to user
  deliverResource(resource);

  // Mark transaction as processed
  processedTransactions.add(txHash);
}

Why this matters: Even though blockchain nonces prevent double-spending, merchants should track which payments they've already fulfilled. This prevents edge cases where:

  • A facilitator sends duplicate settlement notifications
  • Network issues cause duplicate webhook deliveries
  • Multiple facilitators process the same authorization (if incorrectly configured)

Signature Validation

All payment authorizations use EIP-712 typed structured data signatures, providing cryptographic proof that the user authorized the payment.

What EIP-712 Signatures Protect Against

  1. Forgery: Only the user's private key can create a valid signature
  2. Tampering: Changing any field invalidates the signature
  3. Phishing: The signature includes domain binding (verifyingContract address)
  4. Man-in-the-middle: The signature covers all payment details

Server-Side Signature Verification

Facilitators must validate signatures before submitting transactions:

import { verifyTypedData } from 'ethers';

function validatePaymentSignature(payload) {
  const { signature, authorization } = payload.payload;
  const { network } = payload;

  // EIP-712 domain for USDC on Avalanche Fuji
  const domain = {
    name: "USD Coin",
    version: "2",
    chainId: 43113,  // Avalanche Fuji
    verifyingContract: "0x5425890298aed601595a70AB815c96711a31Bc65"
  };

  // EIP-712 type definition
  const types = {
    TransferWithAuthorization: [
      { name: "from", type: "address" },
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
      { name: "validAfter", type: "uint256" },
      { name: "validBefore", type: "uint256" },
      { name: "nonce", type: "bytes32" }
    ]
  };

  // Recover signer address from signature
  const recovered = verifyTypedData(
    domain,
    types,
    authorization,
    signature
  );

  // Verify the signer matches the 'from' address
  if (recovered.toLowerCase() !== authorization.from.toLowerCase()) {
    throw new Error("Invalid signature: signer does not match 'from' address");
  }

  return true;
}

Key validation steps:

  1. Recover the signer address from the signature
  2. Verify it matches the from field in the authorization
  3. Check all other fields (amount, recipient, timing)
  4. Only submit to blockchain if everything validates

On-Chain Signature Verification

The USDC contract performs the same signature validation on-chain when transferWithAuthorization is called. This provides trustless verification—even if the facilitator is malicious, an invalid signature will cause the transaction to revert.

Authorization Timing Validation

Facilitators should validate timing constraints before submitting transactions to avoid wasting gas on expired authorizations:

function validateTiming(authorization) {
  const now = Math.floor(Date.now() / 1000);
  const validAfter = parseInt(authorization.validAfter);
  const validBefore = parseInt(authorization.validBefore);

  if (now < validAfter) {
    throw new Error("Authorization not yet valid");
  }

  if (now >= validBefore) {
    throw new Error("Authorization expired");
  }

  // Optional: Warn if expiring soon
  if (now + 30 >= validBefore) {
    console.warn("Authorization expires in less than 30 seconds");
  }

  return true;
}

This prevents:

  • Submitting transactions that will revert due to timing
  • Wasting gas fees on failed transactions
  • Poor user experience from delayed settlements

Settlement Monitoring

Facilitators monitor the blockchain in real-time to detect when payments are settled and to verify transaction success.

What Facilitators Monitor

After submitting a transferWithAuthorization transaction:

  1. Transaction Confirmation: Wait for the transaction to be included in a block
  2. Transaction Success: Verify the transaction status is 1 (success), not 0 (failure)
  3. Event Emissions: Check that the Transfer event was emitted with correct parameters
  4. Finality: On Avalanche, wait ~1-2 seconds for finality

Webhook Notifications

Most facilitators provide webhook notifications when settlements complete:

// Merchant server receives webhook
app.post('/webhooks/payment-settled', (req, res) => {
  const { transaction, network, payer, amount, success } = req.body;

  if (success) {
    // Verify transaction on-chain (trustless)
    const verified = await verifyTransactionOnChain(transaction, network);

    if (verified) {
      // Fulfill user's order
      fulfillOrder(payer, amount);
    }
  }

  res.status(200).send('OK');
});

Best practice: Always verify webhook data by querying the blockchain directly. Don't trust the webhook alone—use it as a notification, then verify on-chain.

Independent Monitoring

Merchants can also monitor the blockchain directly without relying on facilitators:

import { ethers } from 'ethers';

// Monitor for Transfer events to your address
const provider = new ethers.JsonRpcProvider('https://api.avax-test.network/ext/bc/C/rpc');
const usdcContract = new ethers.Contract(usdcAddress, usdcAbi, provider);

// Listen for incoming payments
usdcContract.on('Transfer', (from, to, value, event) => {
  if (to.toLowerCase() === myAddress.toLowerCase()) {
    console.log(`Received ${value} USDC from ${from}`);
    console.log(`Transaction: ${event.transactionHash}`);

    // Fulfill payment
    fulfillPayment(event.transactionHash);
  }
});

This provides complete independence from facilitators for settlement verification.

Amount Verification

Always verify that the payment amount meets your requirements:

function validatePaymentAmount(authorization, maxAmountRequired) {
  const authorizedAmount = BigInt(authorization.value);
  const requiredAmount = BigInt(maxAmountRequired);

  if (authorizedAmount < requiredAmount) {
    throw new Error(
      `Insufficient payment: required ${requiredAmount}, got ${authorizedAmount}`
    );
  }

  return true;
}

Important: Amounts are in token base units (see Amount Specification for details).

Accept payments equal to or greater than maxAmountRequired to allow for user generosity (tips).

Network Verification

Verify that the payment is on the expected blockchain network:

function validateNetwork(payload, expectedNetwork) {
  if (payload.network !== expectedNetwork) {
    throw new Error(
      `Wrong network: expected ${expectedNetwork}, got ${payload.network}`
    );
  }

  // Also verify the chainId matches the network
  const expectedChainId = getChainId(expectedNetwork);
  // Signature domain includes chainId, so this is validated in signature verification
}

This prevents:

  • Payments on testnets being accepted as mainnet payments
  • Cross-chain replay attacks
  • Merchant configuration errors

Recipient Address Verification

Verify that payments are sent to the correct recipient address:

function validateRecipient(authorization, expectedPayTo) {
  if (authorization.to.toLowerCase() !== expectedPayTo.toLowerCase()) {
    throw new Error(
      `Wrong recipient: expected ${expectedPayTo}, got ${authorization.to}`
    );
  }
}

This is especially important when:

  • Supporting multiple merchants on one facilitator
  • Using different addresses for different services
  • Accepting payments to multiple wallets

Token Contract Verification

Verify that the payment uses the expected token (asset):

function validateAsset(paymentRequest, expectedAsset) {
  if (paymentRequest.asset.toLowerCase() !== expectedAsset.toLowerCase()) {
    throw new Error(
      `Wrong token: expected ${expectedAsset}, got ${paymentRequest.asset}`
    );
  }
}

Why this matters: Attackers could try to pay with worthless tokens that share similar addresses. Always validate the exact contract address.

Example USDC addresses:

  • Avalanche C-Chain: 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E
  • Avalanche Fuji: 0x5425890298aed601595a70AB815c96711a31Bc65
  • Base Sepolia: 0x036CbD53842c5426634e7929541eC2318f3dCF7e

Rate Limiting and Abuse Prevention

Implement rate limiting to prevent abuse:

// Simple rate limiting by address
const paymentsByAddress = new Map();

function checkRateLimit(from, maxPerHour = 10) {
  const now = Date.now();
  const oneHour = 60 * 60 * 1000;

  if (!paymentsByAddress.has(from)) {
    paymentsByAddress.set(from, []);
  }

  const payments = paymentsByAddress.get(from);

  // Remove old payments outside the window
  const recentPayments = payments.filter(time => now - time < oneHour);

  if (recentPayments.length >= maxPerHour) {
    throw new Error(`Rate limit exceeded: max ${maxPerHour} payments per hour`);
  }

  recentPayments.push(now);
  paymentsByAddress.set(from, recentPayments);
}

This prevents:

  • Spam attacks on your API
  • Nonce exhaustion attacks
  • Resource abuse

Summary

x402 security relies on multiple defense layers:

  1. Nonce-based replay prevention: Each authorization is single-use
  2. Time-bounded authorizations: Payments expire after a set time window
  3. EIP-712 signatures: Cryptographic proof of user authorization
  4. On-chain validation: USDC contract verifies all constraints
  5. Transaction hash tracking: Merchants prevent duplicate fulfillment
  6. Amount, network, and asset verification: Validate all payment parameters
  7. Settlement monitoring: Real-time blockchain monitoring for finality

These mechanisms provide trustless, cryptographically secure payments without requiring users to trust facilitators or merchants. The blockchain enforces all security rules.

Additional Resources

Is this guide helpful?