Skip to main content

Overview

This tutorial walks you through executing your first x402 payment from start to finish. You’ll learn how to:
  • Set up your wallet with USDC on Cronos Testnet
  • Make a payment-protected API request
  • Handle the 402 response
  • Generate and submit payment
  • Retry with entitlement
Time to complete: 10 minutes

Prerequisites

Install MetaMask or any Web3 wallet:
Add Cronos Testnet to your wallet:
FieldValue
Network NameCronos Testnet
RPC URLhttps://evm-t3.cronos.org
Chain ID338
Currency SymbolTCRO
Block Explorerhttps://explorer.cronos.org/testnet
Get free testnet CRO from faucet:
  • Visit Cronos Faucet
  • Enter your wallet address
  • Receive 10 TCRO for gas fees
Get testnet USDC:
# USDC Contract: 0xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0
# Contact RelayCore team for testnet USDC
# Or use the faucet endpoint
curl -X POST https://api.relaycore.xyz/faucet \
  -H "Content-Type: application/json" \
  -d '{"address": "YOUR_WALLET_ADDRESS"}'

Step-by-Step Tutorial

Step 1: Install Dependencies

npm install ethers @crypto.com/facilitator-client

Step 2: Initialize Wallet and Facilitator

import { ethers } from 'ethers';
import { Facilitator, CronosNetwork } from '@crypto.com/facilitator-client';

// Connect to Cronos Testnet
const provider = new ethers.JsonRpcProvider('https://evm-t3.cronos.org');

// Initialize wallet (use your private key)
const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, provider);

// Initialize Facilitator
const facilitator = new Facilitator({ 
  network: CronosNetwork.CronosTestnet 
});

console.log('Wallet address:', wallet.address);

Step 3: Check USDC Balance

const USDC_ADDRESS = '0xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0';

const ERC20_ABI = [
  "function balanceOf(address owner) view returns (uint256)",
  "function decimals() view returns (uint8)"
];

const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider);

const balance = await usdc.balanceOf(wallet.address);
const decimals = await usdc.decimals();

console.log('USDC Balance:', ethers.formatUnits(balance, decimals), 'USDC');

Step 4: Make Protected API Request

const API_URL = 'https://api.relaycore.xyz';

// Request a protected resource (will return 402)
const response = await fetch(`${API_URL}/api/perpai/quote`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    pair: 'BTC-USD',
    side: 'long',
    leverage: 10,
    sizeUsd: 1000
  })
});

console.log('Status:', response.status); // 402 Payment Required

Step 5: Handle 402 Response

if (response.status === 402) {
  const paymentChallenge = await response.json();
  
  console.log('Payment Required:');
  console.log('Payment ID:', paymentChallenge.paymentId);
  console.log('Amount:', paymentChallenge.paymentRequirements.maxAmountRequired, 'base units');
  console.log('Recipient:', paymentChallenge.paymentRequirements.payTo);
  console.log('Resource:', paymentChallenge.paymentRequirements.resource);
  
  /*
  Response:
  {
    "error": "Payment Required",
    "paymentId": "pay_1706000000_abc123",
    "paymentRequirements": {
      "scheme": "exact",
      "network": "cronos-testnet",
      "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
      "asset": "0xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0",
      "maxAmountRequired": "10000",
      "maxTimeoutSeconds": 300,
      "resource": "/api/perpai/quote",
      "description": "PerpAI quote aggregation"
    },
    "message": "Payment of 10000 base units required",
    "network": "cronos-testnet"
  }
  */
}

Step 6: Generate Payment Header

const { paymentId, paymentRequirements } = paymentChallenge;

// Generate EIP-3009 authorization
const paymentHeader = await facilitator.generatePaymentHeader({
  to: paymentRequirements.payTo,
  value: paymentRequirements.maxAmountRequired,
  asset: paymentRequirements.asset,
  signer: wallet,
  validAfter: Math.floor(Date.now() / 1000) - 60,
  validBefore: Math.floor(Date.now() / 1000) + 300
});

console.log('Payment header generated');
console.log('Header length:', paymentHeader.length, 'bytes');

Step 7: Submit Payment for Settlement

const settlementResponse = await fetch(`${API_URL}/api/pay`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    paymentId,
    paymentHeader,
    paymentRequirements
  })
});

const settlementResult = await settlementResponse.json();

if (settlementResult.success) {
  console.log('Payment settled successfully!');
  console.log('Transaction Hash:', settlementResult.txHash);
  console.log('Explorer:', `https://explorer.cronos.org/testnet/tx/${settlementResult.txHash}`);
  
  /*
  Response:
  {
    "success": true,
    "paymentId": "pay_1706000000_abc123",
    "txHash": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
    "timestamp": 1706000000000
  }
  */
} else {
  console.error('Payment failed:', settlementResult.error);
}

Step 8: Retry Request with Payment ID

// Retry the original request with payment ID
const retryResponse = await fetch(`${API_URL}/api/perpai/quote`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Payment-Id': paymentId
  },
  body: JSON.stringify({
    pair: 'BTC-USD',
    side: 'long',
    leverage: 10,
    sizeUsd: 1000
  })
});

if (retryResponse.ok) {
  const quote = await retryResponse.json();
  
  console.log('Quote received:');
  console.log('Entry Price:', quote.entryPrice);
  console.log('Liquidation Price:', quote.liquidationPrice);
  console.log('Funding Rate:', quote.fundingRate);
  console.log('Best Venue:', quote.bestVenue);
  
  /*
  Response:
  {
    "pair": "BTC-USD",
    "entryPrice": 42487.32,
    "liquidationPrice": 38238.59,
    "fundingRate": 0.0001,
    "bestVenue": "VVS Finance",
    "sources": [
      { "venue": "VVS Finance", "price": 42487.32, "liquidity": "high" },
      { "venue": "Moonlander", "price": 42495.10, "liquidity": "medium" }
    ]
  }
  */
}

Step 9: Verify On-Chain

// Check transaction on Cronos Explorer
const explorerUrl = `https://explorer.cronos.org/testnet/tx/${settlementResult.txHash}`;
console.log('View transaction:', explorerUrl);

// Verify USDC balance decreased
const newBalance = await usdc.balanceOf(wallet.address);
const spent = balance - newBalance;

console.log('USDC spent:', ethers.formatUnits(spent, decimals), 'USDC');
console.log('New balance:', ethers.formatUnits(newBalance, decimals), 'USDC');

Complete Code Example

import { ethers } from 'ethers';
import { Facilitator, CronosNetwork } from '@crypto.com/facilitator-client';

async function firstPayment() {
  // 1. Setup
  const provider = new ethers.JsonRpcProvider('https://evm-t3.cronos.org');
  const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY, provider);
  const facilitator = new Facilitator({ network: CronosNetwork.CronosTestnet });
  
  const API_URL = 'https://api.relaycore.xyz';
  const USDC_ADDRESS = '0xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0';
  
  console.log('Wallet:', wallet.address);
  
  // 2. Check balance
  const usdc = new ethers.Contract(
    USDC_ADDRESS,
    ['function balanceOf(address) view returns (uint256)'],
    provider
  );
  const balance = await usdc.balanceOf(wallet.address);
  console.log('USDC Balance:', ethers.formatUnits(balance, 6), 'USDC');
  
  // 3. Request protected resource
  const response = await fetch(`${API_URL}/api/perpai/quote`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      pair: 'BTC-USD',
      side: 'long',
      leverage: 10,
      sizeUsd: 1000
    })
  });
  
  if (response.status !== 402) {
    throw new Error('Expected 402 Payment Required');
  }
  
  // 4. Handle 402
  const { paymentId, paymentRequirements } = await response.json();
  console.log('Payment required:', paymentRequirements.maxAmountRequired, 'base units');
  
  // 5. Generate payment header
  const paymentHeader = await facilitator.generatePaymentHeader({
    to: paymentRequirements.payTo,
    value: paymentRequirements.maxAmountRequired,
    asset: paymentRequirements.asset,
    signer: wallet,
    validAfter: Math.floor(Date.now() / 1000) - 60,
    validBefore: Math.floor(Date.now() / 1000) + 300
  });
  
  // 6. Submit payment
  const settlementResponse = await fetch(`${API_URL}/api/pay`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ paymentId, paymentHeader, paymentRequirements })
  });
  
  const settlement = await settlementResponse.json();
  if (!settlement.success) {
    throw new Error(`Payment failed: ${settlement.error}`);
  }
  
  console.log('Payment settled:', settlement.txHash);
  
  // 7. Retry with payment ID
  const retryResponse = await fetch(`${API_URL}/api/perpai/quote`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Payment-Id': paymentId
    },
    body: JSON.stringify({
      pair: 'BTC-USD',
      side: 'long',
      leverage: 10,
      sizeUsd: 1000
    })
  });
  
  const quote = await retryResponse.json();
  console.log('Quote:', quote);
  
  // 8. Verify balance
  const newBalance = await usdc.balanceOf(wallet.address);
  const spent = balance - newBalance;
  console.log('Spent:', ethers.formatUnits(spent, 6), 'USDC');
  
  return quote;
}

// Run
firstPayment()
  .then(quote => console.log('Success!', quote))
  .catch(error => console.error('Error:', error));

Common Issues

Issue 1: Insufficient USDC Balance

Error: Insufficient USDC balance Solution:
# Get testnet USDC from faucet
curl -X POST https://api.relaycore.xyz/faucet \
  -H "Content-Type: application/json" \
  -d '{"address": "YOUR_WALLET_ADDRESS"}'

Issue 2: Invalid Signature

Error: Payment verification failed: Invalid signature Solution:
  • Ensure you’re using the correct wallet
  • Check that signer parameter matches wallet
  • Verify network is Cronos Testnet (338)

Issue 3: Nonce Already Used

Error: Nonce already used Solution:
  • Generate a fresh payment header
  • Don’t reuse payment headers
  • Each payment needs a unique nonce

Issue 4: Authorization Expired

Error: Authorization expired Solution:
// Increase validity window
const paymentHeader = await facilitator.generatePaymentHeader({
  // ...
  validAfter: Math.floor(Date.now() / 1000) - 60,
  validBefore: Math.floor(Date.now() / 1000) + 600 // 10 minutes
});

Next Steps

Session Escrow

Create session for multiple payments

SDK Integration

Use RelayAgent SDK

MCP Tools

Use x402 via MCP server

Build Service

Create paid service