Skip to main content

Performance optimization

1. Warm up cache

Warm up token detection cache at app startup:
const server = new X402Server({ client, facilitator });

// Warm up common tokens (non-blocking)
server.initialize([
  "0x25d066c4C68C8A6332DfDB4230263608305Ca991", // USDC
  "0xcea4eaef42afd4d6e12660b59018e90fa3ab28f4", // DAI
]).then(result => {
  if (result.success) {
    console.log(`✅ Cached ${result.detected} tokens`);
  }
});

2. Fast mode

Skip detection for tokens with a known payment type:
const requirements = await server.createRequirements({
  asset: "0x25d066c4C68C8A6332DfDB4230263608305Ca991",
  maxAmountRequired: "1000000",
  paymentType: "permit",
  autoDetect: false, // <1ms
});

3. Reuse requirements

For fixed-amount APIs, reuse the requirements object:
// Create once, reuse many times
const cachedRequirements = await server.createRequirements({
  asset: "0xUSDC",
  maxAmountRequired: "1000000",
});

app.post("/api", async (req, res) => {
  const result = await server.process(
    req.headers["x-payment"] as string,
    cachedRequirements // reuse
  );
  // ...
});

Security

1. Amount cap

The client must set a maximum payment amount:
const fetchWithPay = wrapFetchWithPayment(
  fetch,
  client,
  "10000000" // pay up to 10 USDC
);

Multi-chain deployment

Create separate instances per chain

// BSC
const bscClient = createPublicClient({
  chain: bsc,
  transport: http(),
});
const bscServer = new X402Server({ 
  client: bscClient, 
  facilitator
});

// Polygon
const polygonClient = createPublicClient({
  chain: polygon,
  transport: http(),
});
const polygonServer = new X402Server({ 
  client: polygonClient, 
  facilitator
});

// Choose by request
app.post("/api", async (req, res) => {
  const network = req.headers["x-network"];
  const server = network === "polygon" ? polygonServer : bscServer;
  
  const requirements = await server.createRequirements({
    asset: 'xxx',
    maxAmountRequired: "1000000",
  });
  
  const result = await server.process(
    req.headers["x-payment"] as string,
    requirements
  );
  
  // ...
});

Error handling

Handle by error stage

Provide different handling strategies for different stages:
const result = await server.process(paymentHeader, requirements);

if (!result.success) {
  switch (result.errorStage) {
    case "parse":
      // Payment header format error - client issue
      console.error("Invalid payment header");
      return res.status(402).json({
        ...result.response,
        userMessage: "Invalid payment info, please retry",
      });
      
    case "verify":
      // Signature verification failed - client issue
      console.error("Signature verification failed");
      return res.status(402).json({
        ...result.response,
        userMessage: "Payment verification failed, please re-sign",
      });
      
    case "settle":
      // On-chain settlement failed - server issue
      console.error("Settlement failed:", result.error);
      // Consider alerting or retry mechanisms
      return res.status(500).json({
        ...result.response,
        userMessage: "Service temporarily unavailable, please try again later",
      });
  }
}

Unified error handling

app.post("/api", async (req, res) => {
  try {
    const requirements = await server.createRequirements({
      asset: "0xUSDC",
      maxAmountRequired: "1000000",
    });
    
    const result = await server.process(
      req.headers["x-payment"] as string,
      requirements
    );
    
    if (!result.success) {
      // Log by error stage
      switch (result.errorStage) {
        case "parse":
          console.error("Payment header parse failed:", result.response.error);
          break;
        case "verify":
          console.error("Signature verification failed:", result.response.error);
          break;
        case "settle":
          console.error("On-chain settlement failed:", result.error);
          break;
      }
      
      // Return corresponding status
      return res.status(result.status).json(result.response);
    }
    
    res.json({ data: "success" });
  } catch (error) {
    console.error("Unexpected error:", error);
    res.status(500).json({ 
      error: "Internal server error",
      message: error instanceof Error ? error.message : "Unknown error"
    });
  }
});

Client-side error handling

try {
  const response = await fetchWithPay(apiUrl, {
    method: "POST",
  });
  
  if (!response.ok) {
    if (response.status === 402) {
      const error = await response.json();
      console.error("Payment required:", error);
    } else {
      console.error("Request failed:", response.status);
    }
    return;
  }
  
  const data = await response.json();
  console.log("Success:", data);
} catch (error) {
  console.error("Error:", error);
}

Server-side error handling

app.post("/api", async (req, res) => {
  try {
    const result = await server.process(
      req.headers["x-payment"] as string,
      requirements
    );
    
    if (!result.success) {
      return res.status(result.status).json(result.response);
    }
    
    res.json({ data: "content" });
  } catch (error) {
    console.error("Payment processing error:", error);
    res.status(500).json({
      error: "Internal server error",
      message: error instanceof Error ? error.message : "Unknown error",
    });
  }
});

FAQs

Q1: How to choose the right authorization type?

A: Use auto detection (default) or follow suggestions:
  • EIP-3009: Token-native support (e.g., USDC) — most efficient
  • EIP-2612 Permit: Standard ERC-20 — great compatibility
  • Permit2: Need generalized authorization management — most flexible

Q2: How to handle tokens with different decimals?

A: Be careful with units (wei):
// USDC (6 decimals): 1 USDC = 1000000
const usdcAmount = "1000000"; // 1 USDC

// DAI (18 decimals): 1 DAI = 1000000000000000000
const daiAmount = "1000000000000000000"; // 1 DAI

Q3: How to handle payment failures?

A: Use different strategies for different stages:
const result = await server.process(paymentHeader, requirements);

if (!result.success) {
  // Inspect error stage
  switch (result.errorStage) {
    case "parse":
      // Payment header format error - client issue
      console.error("Invalid payment header format");
      return res.status(402).json(result.response);
      
    case "verify":
      // Signature verification failed - client issue
      console.error("Signature verification failed");
      return res.status(402).json(result.response);
      
    case "settle":
      // On-chain settlement failed - server issue
      console.error("Settlement failed:", result.error);
      // Likely needs alerting or retry
      return res.status(500).json(result.response);
  }
}

Q4: When should I return 500 instead of 402?

A:
  • 402: Client-side issues (parse/verify failed) — the client must fix
  • 500: Server-side issues (settle failed) — may be transient (network, gas, etc.)
Settlement failures are typically temporary server issues; return 500 and suggest retrying later.

What’s next