// hooks/useX402Payment.ts
import {useState} from 'react';
import {useSignTypedData, useWallets} from '@privy-io/react-auth';
// x402 types
interface PaymentRequirements {
scheme: string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
mimeType: string;
payTo: string;
maxTimeoutSeconds: number;
asset: string;
extra: {
name: string;
version: string;
chainId: string;
};
}
interface PaymentRequiredResponse {
x402Version: number;
accepts: PaymentRequirements[];
error?: string;
}
export function useX402Payment() {
const {signTypedData} = useSignTypedData();
const {wallets} = useWallets();
const [isLoading, setIsLoading] = useState(false);
async function buildXPaymentHeader(requirements: PaymentRequirements): Promise<string> {
if (!wallets[0]) {
throw new Error('No wallet available');
}
const wallet = wallets[0];
const address = wallet.address;
// Generate a random 32-byte nonce
const nonceBytes = crypto.getRandomValues(new Uint8Array(32));
const nonce = `0x${Array.from(nonceBytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;
// Calculate time window
const validAfter = Math.floor(Date.now() / 1000);
const validBefore = validAfter + requirements.maxTimeoutSeconds;
// Map network to chainId (fallback if not in extra)
const networkToChainId: Record<string, number> = {
base: 8453,
'base-sepolia': 84532,
ethereum: 1,
sepolia: 11155111
};
const chainIdNum = requirements.extra?.chainId
? Number(requirements.extra.chainId)
: networkToChainId[requirements.network] || 84532;
// Construct EIP-712 typed data for USDC transferWithAuthorization
const domain = {
name: requirements.extra.name,
version: requirements.extra.version,
chainId: chainIdNum,
verifyingContract: requirements.asset as `0x${string}`
};
const types = {
TransferWithAuthorization: [
{name: 'from', type: 'address'},
{name: 'to', type: 'address'},
{name: 'value', type: 'uint256'},
{name: 'validAfter', type: 'uint256'},
{name: 'validBefore', type: 'uint256'},
{name: 'nonce', type: 'bytes32'}
]
};
const message = {
from: address,
to: requirements.payTo,
value: requirements.maxAmountRequired,
validAfter,
validBefore,
nonce
};
// Sign with Privy's signTypedData
const {signature} = await signTypedData(
{
domain,
types,
primaryType: 'TransferWithAuthorization',
message
},
{
address: wallet.address
}
);
// Construct x402 payment payload matching x402 spec format
const xPayment = {
x402Version: 1,
scheme: requirements.scheme,
network: requirements.network,
payload: {
authorization: {
from: address,
to: requirements.payTo,
value: requirements.maxAmountRequired,
validAfter: validAfter.toString(),
validBefore: validBefore.toString(),
nonce
},
signature
}
};
return Buffer.from(JSON.stringify(xPayment)).toString('base64');
}
async function fetchWithPayment(url: string, options?: RequestInit): Promise<Response> {
setIsLoading(true);
try {
// First request - may receive 402
let response = await fetch(url, options);
if (response.status === 402) {
// Parse payment requirements
const paymentRequired: PaymentRequiredResponse = await response.json();
if (!paymentRequired.accepts || paymentRequired.accepts.length === 0) {
throw new Error('No payment methods accepted');
}
// Select first payment requirement (typically USDC on Base)
const requirements = paymentRequired.accepts[0];
// Build X-PAYMENT header
const xPaymentHeader = await buildXPaymentHeader(requirements);
// Retry with payment
response = await fetch(url, {
...options,
headers: {
...options?.headers,
'X-PAYMENT': xPaymentHeader
}
});
// Check if payment was successful
if (!response.ok && response.status !== 402) {
throw new Error(`Payment failed: ${response.statusText}`);
}
}
return response;
} finally {
setIsLoading(false);
}
}
return {
buildXPaymentHeader,
fetchWithPayment,
isLoading
};
}