Skip to main content

Overview

The x402 protocol enables HTTP-based payments where services return 402 Payment Required responses, clients sign EIP-3009 authorizations, and the Facilitator SDK settles payments on-chain.

Payment Flow

1

Client Requests Resource

GET /api/perpai/quote HTTP/1.1
Host: api.relaycore.xyz
2

Server Returns 402

HTTP/1.1 402 Payment Required
{
  "paymentId": "pay_1234567890",
  "paymentRequirements": {
    "payTo": "0x...",
    "maxAmountRequired": "1000000",
    "asset": "0x...",
    "network": "cronos-testnet",
    "resource": "/api/perpai/quote"
  }
}
3

Client Signs Authorization

const message = JSON.stringify({
  from: walletAddress,
  to: paymentRequirements.payTo,
  value: paymentRequirements.maxAmountRequired,
  validAfter: Math.floor(Date.now() / 1000),
  validBefore: Math.floor(Date.now() / 1000) + 300,
  nonce: Date.now()
});

const signature = await wallet.signMessage(message);
4

Client Submits Payment

POST /api/pay HTTP/1.1
Content-Type: application/json

{
  "paymentId": "pay_1234567890",
  "paymentHeader": "...",
  "paymentRequirements": {...}
}
5

Facilitator Settles On-Chain

Server verifies signature and settles USDC transfer via Facilitator SDK.
const result = await facilitatorService.settlePayment(
  paymentHeader,
  paymentRequirements
);
6

Server Grants Entitlement

HTTP/1.1 200 OK
{
  "success": true,
  "paymentId": "pay_1234567890",
  "txHash": "0x..."
}
7

Client Retries with Payment ID

GET /api/perpai/quote HTTP/1.1
X-Payment-Id: pay_1234567890
8

Server Returns Content

HTTP/1.1 200 OK
{
  "quote": {
    "entryPrice": 2450.50,
    "liquidationPrice": 2205.45
  }
}

Implementation

Protecting Routes

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) => {
    // Route is protected - only executes if payment verified
    const quote = await getQuote(req.body);
    res.json(quote);
  }
);

Settlement Endpoint

import { facilitatorService } from '@/services/x402/facilitator-service';

router.post('/pay', async (req, res) => {
  const { paymentId, paymentHeader, paymentRequirements } = req.body;

  const result = await facilitatorService.settlePayment(
    paymentHeader,
    paymentRequirements
  );

  if (!result.success) {
    return res.status(400).json({ error: result.error });
  }

  // Record payment in database
  await supabase.from('payments').insert({
    payment_id: paymentId,
    tx_hash: result.txHash,
    amount: paymentRequirements.maxAmountRequired,
    status: 'settled'
  });

  res.json({ success: true, txHash: result.txHash });
});

Entitlement Caching

Payments are cached in memory for fast subsequent requests:
const entitlementCache = new Map<string, {
  paymentId: string;
  resourceUrl: string;
  userAddress: string;
  timestamp: number;
}>();

// Check cache before requiring payment
if (paymentId && entitlementCache.has(paymentId)) {
  const entitlement = entitlementCache.get(paymentId);
  if (entitlement.resourceUrl === params.resourceUrl) {
    req.isEntitled = true;
    return next();
  }
}

Security Features

All payment headers are verified using EIP-3009 signature recovery before settlement.
Nonces are tracked to prevent replay attacks. Each payment uses a unique nonce.
Payment authorizations include validBefore timestamp. Expired payments are rejected.
maxAmountRequired enforces maximum payment amount. Facilitator rejects overpayments.

Network Configuration

NetworkChain IDFacilitator URL
Testnet338https://facilitator.cronos.org
Mainnet25https://facilitator.cronos.org

Next Steps