Skip to main content

Overview

The x402 protocol is RelayCore’s implementation of HTTP 402 Payment Required, enabling gasless payments where agents pay for services without submitting blockchain transactions. Instead, agents sign EIP-3009 authorizations off-chain, and the Crypto.com Facilitator SDK settles payments on Cronos. Key Benefits:
  • Gasless: Agents never pay gas fees
  • Instant: No waiting for block confirmations
  • Secure: EIP-3009 standard with signature verification
  • Scalable: Session-based budgets enable micro-payments

Complete Payment Flow

1

1. Agent Requests Protected Resource

Agent calls a protected API endpoint without payment:
POST /api/perpai/quote HTTP/1.1
Host: api.relaycore.xyz
Content-Type: application/json

{
  "pair": "BTC-USD",
  "side": "long",
  "leverage": 10,
  "sizeUsd": 1000
}
2

2. Server Returns 402 Payment Required

The requirePayment middleware intercepts the request and returns 402:
HTTP/1.1 402 Payment Required
{
  "error": "Payment Required",
  "paymentId": "pay_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"
}
Payment Requirements Breakdown:
  • payTo: Merchant wallet address (service provider)
  • asset: USDC contract address on Cronos Testnet
  • maxAmountRequired: Amount in base units (10000 = 0.01 USDC with 6 decimals)
  • resource: The protected endpoint URL
3

3. Agent Generates EIP-3009 Authorization

Agent uses the Facilitator SDK to generate a signed payment header:
import { Facilitator, CronosNetwork } from '@crypto.com/facilitator-client';
import { ethers } from 'ethers';

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

const wallet = new ethers.Wallet(privateKey, provider);

// Generate payment header with signature
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
});
What’s in the Payment Header:
  • EIP-3009 TransferWithAuthorization parameters
  • EIP-712 signature proving authorization
  • Nonce from USDC contract (prevents replay)
  • Validity window (5 minutes)
4

4. Agent Submits Payment for Settlement

Agent sends the signed authorization to the settlement endpoint:
POST /api/pay HTTP/1.1
Content-Type: application/json

{
  "paymentId": "pay_abc123",
  "paymentHeader": "0x...",
  "paymentRequirements": {
    "scheme": "exact",
    "network": "cronos-testnet",
    "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    "asset": "0xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0",
    "maxAmountRequired": "10000",
    "maxTimeoutSeconds": 300,
    "resource": "/api/perpai/quote"
  }
}
5

5. Facilitator Verifies and Settles On-Chain

Server uses Facilitator SDK to verify signature and execute transfer:
// From: src/services/x402/facilitator-service.ts
const verifyRequest = facilitator.buildVerifyRequest(
  paymentHeader,
  paymentRequirements
);

// Verify EIP-3009 signature
const verifyResult = await facilitator.verifyPayment(verifyRequest);
if (!verifyResult.isValid) {
  throw new Error('Invalid signature');
}

// Settle payment on-chain (Facilitator pays gas)
const settleResult = await facilitator.settlePayment(verifyRequest);
// Returns: { txHash: '0x...', success: true }
Gasless Magic: The Facilitator executes transferWithAuthorization on the USDC contract, paying gas fees while transferring USDC from agent to merchant.
6

6. Server Records Payment and Grants Entitlement

Payment is recorded in database and cached in memory:
// Store in Supabase
await supabase.from('payments').insert({
  payment_id: paymentId,
  tx_hash: settleResult.txHash,
  from_address: userAddress,
  to_address: paymentRequirements.payTo,
  amount: paymentRequirements.maxAmountRequired,
  resource_url: paymentRequirements.resource,
  status: 'settled'
});

// Cache entitlement for fast lookups
entitlementCache.set(paymentId, {
  paymentId,
  resourceUrl: paymentRequirements.resource,
  userAddress,
  timestamp: Date.now()
});
Server responds with success:
HTTP/1.1 200 OK
{
  "success": true,
  "paymentId": "pay_abc123",
  "txHash": "0x1234...abcd",
  "timestamp": 1706000000000
}
7

7. Agent Retries Request with Payment ID

Agent retries the original request with the payment ID header:
POST /api/perpai/quote HTTP/1.1
X-Payment-Id: pay_abc123
Content-Type: application/json

{
  "pair": "BTC-USD",
  "side": "long",
  "leverage": 10,
  "sizeUsd": 1000
}
8

8. Server Verifies Entitlement and Returns Content

Middleware checks entitlement cache and database:
// Check cache first (fast path)
if (entitlementCache.has(paymentId)) {
  const entitlement = entitlementCache.get(paymentId);
  if (entitlement.resourceUrl === resourceUrl) {
    req.isEntitled = true;
    return next(); // Grant access
  }
}

// Check database (fallback)
const { data } = await supabase
  .from('payments')
  .select('*')
  .eq('payment_id', paymentId)
  .eq('resource_url', resourceUrl)
  .eq('status', 'settled')
  .single();

if (data) {
  req.isEntitled = true;
  return next();
}
Server returns the protected content:
HTTP/1.1 200 OK
{
  "quote": {
    "pair": "BTC-USD",
    "entryPrice": 42500.50,
    "liquidationPrice": 38250.45,
    "fundingRate": 0.0001,
    "bestVenue": "VVS Finance"
  }
}

Session-Based Payments

For agents making multiple requests, session escrow eliminates repeated x402 flows:
1

1. Create Session with Budget

POST /api/sessions/create
{
  "maxSpend": "100.00",
  "duration": 86400
}
Returns 402 for session deposit.
2

2. Pay Session Deposit via x402

Agent pays once to fund the session budget.
3

3. Use Session for All Requests

POST /api/perpai/quote
X-Session-Id: session_xyz789

# Payment automatically deducted from session budget
# No 402, no signing, instant execution
Budget Tracking:
const remaining = session.deposited - session.released;
if (remaining >= amountRequired) {
  session.released += amountRequired;
  // Execute service
}
Session Benefits:
  • One payment, unlimited requests (within budget)
  • Real-time balance tracking
  • Automatic refunds on expiration
  • Perfect for autonomous agents

Implementation Guide

Protecting Routes with x402

// src/api/perpai-routes.ts
import { requirePayment } from '@/services/x402/payment-middleware';

router.post('/quote',
  requirePayment({
    merchantAddress: process.env.PAYMENT_RECIPIENT_ADDRESS!,
    amount: '10000', // 0.01 USDC
    resourceUrl: '/api/perpai/quote',
  }),
  async (req, res) => {
    // This code only runs if payment is verified
    const quote = await perpaiService.getQuote(req.body);
    res.json(quote);
  }
);

Payment Settlement Endpoint

// src/api/payment-routes.ts
import { handlePaymentSettlement } from '@/services/x402/payment-middleware';

router.post('/pay', handlePaymentSettlement);
The handlePaymentSettlement function:
  1. Validates payment parameters
  2. Builds Facilitator verify request
  3. Verifies EIP-3009 signature
  4. Settles payment on-chain
  5. Records in database
  6. Caches entitlement
  7. Returns transaction hash

Entitlement Caching

// In-memory cache for fast lookups
const entitlementCache = new Map<string, {
  paymentId: string;
  resourceUrl: string;
  userAddress: string;
  timestamp: number;
}>();

// Cache expires after 24 hours
setInterval(() => clearExpiredEntitlements(24 * 60 * 60 * 1000), 60 * 60 * 1000);

Security Guarantees

Every payment header contains an EIP-712 signature that proves:
  • The payer authorized the exact amount
  • The payer authorized the exact recipient
  • The authorization is time-bound
  • The nonce is unique (prevents replay)
The Facilitator SDK verifies all parameters before settlement.
USDC contract maintains a nonce counter for each address. Each TransferWithAuthorization must use the next sequential nonce. This prevents:
  • Replay attacks
  • Double-spending
  • Authorization reuse
Every authorization includes:
  • validAfter: Earliest timestamp for execution
  • validBefore: Latest timestamp for execution
Expired authorizations are rejected by the USDC contract.
The maxAmountRequired field enforces the exact payment amount. The Facilitator rejects any attempt to:
  • Overpay
  • Underpay
  • Change the amount after signing
Each payment is bound to a specific resource URL. Entitlements are only valid for the exact resource that was paid for.

Network Configuration

NetworkChain IDUSDC ContractFacilitator
Cronos Testnet3380xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0Crypto.com Facilitator SDK
Cronos Mainnet250xc21223249CA28397B4B6541dfFaEcC539BfF0c59Crypto.com Facilitator SDK

Environment Variables

# Required for x402 settlement
WALLET_PRIVATE_KEY=0x...           # Server wallet for receiving payments
PAYMENT_RECIPIENT_ADDRESS=0x...    # Merchant address
USDC_TOKEN_ADDRESS=0xc01efAaF...   # USDC contract on Cronos Testnet
VITE_CRONOS_NETWORK=cronos-testnet # Network selection

Indexer Integration

The Payment Indexer runs every 5 minutes to:
  1. Query Cronos Explorer API for recent USDC transfers
  2. Match tx_hash to payments table records
  3. Update block_number and status fields
  4. Trigger reputation updates for services
// src/services/indexer/payment-indexer.ts
export async function indexPayments() {
  const recentPayments = await supabase
    .from('payments')
    .select('*')
    .is('block_number', null)
    .order('timestamp', { ascending: false })
    .limit(100);

  for (const payment of recentPayments.data) {
    const tx = await cronosExplorer.getTransaction(payment.tx_hash);
    if (tx.blockNumber) {
      await supabase
        .from('payments')
        .update({ block_number: tx.blockNumber })
        .eq('payment_id', payment.payment_id);
    }
  }
}

Error Handling

ErrorCauseResolution
Insufficient USDC balanceAgent wallet lacks USDCFund wallet with USDC
Invalid signatureSignature verification failedRegenerate payment header
Nonce already usedReplay attack or stale nonceGet fresh nonce from contract
Authorization expiredvalidBefore timestamp passedGenerate new authorization
Payment already settledDuplicate payment IDUse new payment ID

Next Steps

Session Escrow

Learn about gasless session budgets

SDK Integration

Integrate x402 in your agent

MCP Tools

Use x402 via MCP server

First Payment Guide

Complete tutorial with code