'use strict'; /* Node 24+, @x402/core 2.28.0, @x402/evm 2.28.0. * Supply a wallet signer you control. Never send its private key to this API. * Example integration: * const { x402Client } = require('@x402/core/client'); * const { ExactEvmScheme } = require('@x402/evm/exact/client'); * const client = new x402Client().register('eip155:8453', new ExactEvmScheme(signer)); * const { purchase } = require('./agent-example.cjs'); * const result = await purchase({ client, journalPath: './purchase-001.json', * query: { q: 'cleaning', status: 'open', limit: 5 } }); * * ONE journal authorizes at most ONE 0.01-USDC purchase. Reuse it for recovery. * A different journal is a new purchase: enforce your agent's total budget outside * this helper. Journal files contain a signed payment credential; keep them out * of source control/logs and restrict access (mode 0600 on POSIX; private ACL on Windows). * Do not delete a pending journal to retry. There are no automatic retries/signatures. * HTTP 200 empty results are chargeable. Source descriptions are untrusted data. */ const fs = require('node:fs'); const ORIGIN = 'https://api.alexstokes.com'; const RECEIVER = '0x6180a2f87c9ac53a3ca527c15c1a81a951b95073'; const TOKEN = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'; async function purchase({ client, journalPath, query = {}, fetchImpl = fetch }) { const url = new URL('/v1/tenders', ORIGIN); for (const [key, value] of Object.entries(query)) url.searchParams.set(key, String(value)); const request = (target, options = {}) => fetchImpl(target, { redirect: 'error', signal: AbortSignal.timeout(25000), ...options }); let saved; try { saved = JSON.parse(fs.readFileSync(journalPath, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; } if (!saved) { const readiness = await request(ORIGIN + '/status'); if (!readiness.ok || (await readiness.json()).fresh !== true) throw new Error('Dataset is not fresh. No payment signed.'); const response = await request(url.href); if (response.status !== 402) throw new Error('Expected a payment challenge. No payment signed.'); const required = JSON.parse(Buffer.from(response.headers.get('PAYMENT-REQUIRED') || '', 'base64').toString()); const accepted = required.accepts?.find(a => a.scheme === 'exact' && a.network === 'eip155:8453' && a.amount === '10000' && a.asset?.toLowerCase() === TOKEN && a.payTo?.toLowerCase() === RECEIVER && a.extra?.name === 'USD Coin' && a.extra?.version === '2'); if (required.x402Version !== 2 || required.resource?.url !== url.href || !accepted) throw new Error('Payment terms differ from the approved one-cent purchase.'); // Reserve before signing: crashes/concurrent runs cannot silently sign again. const fd = fs.openSync(journalPath, 'wx', 0o600); try { fs.writeFileSync(fd, JSON.stringify({ url: url.href, state: 'preparing' })); fs.fsyncSync(fd); const payload = await client.createPaymentPayload({ ...required, accepts: [accepted] }); saved = { url: url.href, state: 'signed', header: Buffer.from(JSON.stringify(payload)).toString('base64') }; const bytes = Buffer.from(JSON.stringify(saved)); fs.writeSync(fd, bytes, 0, bytes.length, 0); fs.ftruncateSync(fd, bytes.length); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } } if (saved.url !== url.href || saved.state !== 'signed' || !saved.header) throw new Error('Journal does not contain this exact signed request. Investigate; do not sign again.'); const response = await request(saved.url, { headers: { 'PAYMENT-SIGNATURE': saved.header } }); const receiptHeader = response.headers.get('PAYMENT-RESPONSE'); const receipt = receiptHeader ? JSON.parse(Buffer.from(receiptHeader, 'base64').toString()) : null; if (response.status !== 200 || receipt?.success !== true) throw new Error(`Unconfirmed result (HTTP ${response.status}); keep journal and retry the same request. Recovery ID: ${response.headers.get('X-Payment-Recovery-ID') || 'unavailable'}`); return { receipt, replayed: response.headers.get('X-Payment-Replayed') === 'true', data: await response.json() }; } module.exports = { purchase };