Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.relaycore.xyz/llms.txt

Use this file to discover all available pages before exploring further.

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