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: Field Value Network Name Cronos Testnet RPC URL https://evm-t3.cronos.orgChain ID 338Currency Symbol TCROBlock Explorer https://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"
}
*/
}
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