// Builds an x402 "exact" EVM payment header (EIP-3009 transferWithAuthorization) for v1 or v2 terms.
// The caller passes a viem account; the private key stays with the caller and is never logged.
import type { LocalAccount } from 'viem';
const CHAIN_IDS: Record<string, number> = { base: 8453, 'base-sepolia': 84532, 'eip155:8453': 8453, 'eip155:84532': 84532 };
const randomNonce = (): `0x${string}` => {
const b = new Uint8Array(32);
crypto.getRandomValues(b);
return `0x${Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('')}`;
};
const b64 = (v: unknown) => {
const bytes = new TextEncoder().encode(JSON.stringify(v));
let s = '';
for (const x of bytes) s += String.fromCharCode(x);
return btoa(s);
};
export async function exactPaymentHeader(
terms: { x402Version: number; accepts: Array<Record<string, unknown>>; resource?: unknown },
account: LocalAccount,
opts: { maxAtomic?: bigint; now?: () => number } = {},
): Promise<string> {
const req = terms.accepts.find((a) => a.scheme === 'exact' && String(a.network) in CHAIN_IDS);
if (!req) throw new Error('no exact EVM payment option on Base in the 402 terms');
const value = String(req.amount ?? req.maxAmountRequired);
if (opts.maxAtomic !== undefined && BigInt(value) > opts.maxAtomic) throw new Error(`refusing to pay ${value} atomic USDC, above the cap ${opts.maxAtomic}`);
const chainId = CHAIN_IDS[String(req.network)]!;
const extra = (req.extra ?? {}) as { name?: string; version?: string };
const now = Math.floor((opts.now ?? Date.now)() / 1000);
const authorization = {
from: account.address,
to: String(req.payTo) as `0x${string}`,
value,
validAfter: String(now - 600),
validBefore: String(now + Number(req.maxTimeoutSeconds ?? 60)),
nonce: randomNonce(),
};
const signature = await account.signTypedData({
domain: { name: extra.name ?? 'USD Coin', version: extra.version ?? '2', chainId, verifyingContract: String(req.asset) as `0x${string}` },
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' },
],
},
primaryType: 'TransferWithAuthorization',
message: {
from: authorization.from, to: authorization.to, value: BigInt(value),
validAfter: BigInt(authorization.validAfter), validBefore: BigInt(authorization.validBefore), nonce: authorization.nonce,
},
});
const payload = { signature, authorization };
if (Number(terms.x402Version) >= 2) return b64({ x402Version: 2, resource: terms.resource, accepted: req, payload });
return b64({ x402Version: 1, scheme: 'exact', network: req.network, payload });
}
agentgate-source: code mirror on Cloudflare Pages. All repos