// Node.js 20+. Preview is free; --pay authorizes at most one purchase per run ($0.005 Base, $0.01 Algorand). // Base: npm install @x402/core@2.21.0 @x402/evm@2.21.0 viem // Algorand: npm install @x402/core@2.21.0 @x402/avm@2.21.0 import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { createHash } from 'node:crypto'; const base = 'https://api.agentwork.run'; const networks = { base: { network: 'eip155:8453', payTo: '0x1cfc6489bc9703a2a49715b8a746d8c6075f3a80', asset: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', amount: '5000', usdc: '0.005' }, algorand: { network: 'algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', payTo: 'pfhylvhx77annqvf67lk6evyb34d5vdiuowlol5sx367m366dbloqqaoca', asset: '31566704', amount: '10000', usdc: '0.01' }, }; const hash = text => createHash('sha256').update(text).digest('hex'); async function paymentClient(network, required, allowed) { const { x402Client } = await import('@x402/core/client'); const { x402HTTPClient } = await import('@x402/core/http'); const client = new x402Client(); if (network === 'base') { const key = process.env.EVM_PRIVATE_KEY; if (!/^0x[0-9a-fA-F]{64}$/.test(key || '')) throw Error('Set EVM_PRIVATE_KEY locally.'); const { ExactEvmScheme } = await import('@x402/evm/exact/client'); const { privateKeyToAccount } = await import('viem/accounts'); client.register(networks.base.network, new ExactEvmScheme(privateKeyToAccount(key))); } else { const key = process.env.AVM_PRIVATE_KEY; if (Buffer.from(key || '', 'base64').length !== 64) throw Error('Set AVM_PRIVATE_KEY locally to a base64 Ed25519 secret key.'); const { ExactAvmScheme, toClientAvmSigner } = await import('@x402/avm'); client.register(networks.algorand.network, new ExactAvmScheme(toClientAvmSigner(key))); } client.registerPolicy((_version, terms) => terms.filter(allowed)); const http = new x402HTTPClient(client); const payload = await http.createPaymentPayload(required); if (payload.resource?.url !== required.resource.url) throw Error('Payment resource mismatch.'); return { headers: http.encodePaymentSignatureHeader(payload), receipt: response => http.getPaymentSettleResponse(name => response.headers.get(name)) }; } export async function runMonitor({ url, stateFile, network = 'base', pay = false }, { fetchImpl = fetch, createPayment = paymentClient } = {}) { if (!networks[network]) throw Error('Network must be base or algorand.'); const parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password) throw Error('Use a public HTTP/HTTPS URL without credentials.'); url = parsed.href; if (!stateFile) throw Error('Specify a local baseline JSON file.'); stateFile = path.resolve(stateFile); const pendingFile = stateFile + '.pending'; const lockFile = stateFile + '.lock'; const lock = fs.openSync(lockFile, 'wx', 0o600); try { if (fs.existsSync(pendingFile)) throw Error('Unresolved prior purchase: inspect ' + pendingFile + ' and its settlement before retrying.'); const prior = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : null; if (prior && (prior.url !== url || prior.max_chars !== 20000 || typeof prior.text !== 'string' || hash(prior.text) !== prior.text_sha256)) throw Error('Baseline does not match this URL or is corrupt. Use a separate file for each URL.'); const endpoint = base + (prior ? '/v1/web/changes' : '/v1/web/read'); const body = JSON.stringify({ url, max_chars: 20000, ...(prior ? { previous_text: prior.text, previous_sha256: prior.text_sha256 } : {}) }); const request = extra => fetchImpl(endpoint, { method: 'POST', redirect: 'error', headers: { 'Content-Type': 'application/json', 'User-Agent': 'AgentWorkMonitor/1.0', ...extra }, body, signal: AbortSignal.timeout(45000) }); const response = await request({}); if (response.status !== 402) throw Error('Expected a payment challenge; HTTP ' + response.status); const required = JSON.parse(Buffer.from(response.headers.get('payment-required') || '', 'base64').toString()); const rail = networks[network]; const allowed = terms => terms.scheme === 'exact' && terms.network === rail.network && terms.amount === rail.amount && String(terms.payTo).toLowerCase() === rail.payTo && String(terms.asset).toLowerCase() === rail.asset; if (required.x402Version !== 2 || required.resource?.url !== endpoint || !Array.isArray(required.accepts) || !required.accepts.some(allowed)) throw Error('Unexpected payment terms; no signature sent.'); if (!pay) return { preview: true, endpoint, url, network, maximum_usdc: rail.usdc, action: prior ? 'check_changes' : 'create_baseline', instruction: 'Add --pay to authorize one purchase. No background monitoring is started.' }; const payment = await createPayment(network, required, allowed); // Persist intent BEFORE sending a signature. Ambiguous failures block automatic repurchase. fs.writeFileSync(pendingFile, JSON.stringify({ started_at: new Date().toISOString(), endpoint, url, network, maximum_usdc: rail.usdc }), { flag: 'wx', mode: 0o600 }); const result = await request(payment.headers); // One signed request; never retry here. const receipt = payment.receipt(result); const text = await result.text(); fs.writeFileSync(pendingFile, JSON.stringify({ endpoint, url, network, http_status: result.status, receipt, response: text }), { mode: 0o600 }); if (result.status !== 200 || receipt?.success !== true) throw Error('Purchase not confirmed; inspect ' + pendingFile + ' before another attempt.'); const data = JSON.parse(text); const currentText = data.text ?? (data.comparison?.changed === false ? prior?.text : undefined); if (data.ok !== true || typeof currentText !== 'string' || hash(currentText) !== data.text_sha256) throw Error('Unexpected result; receipt retained in ' + pendingFile); const state = { url, max_chars: 20000, text: currentText, text_sha256: data.text_sha256, fetched_at: data.fetched_at, last_receipt: receipt }; fs.writeFileSync(stateFile + '.tmp', JSON.stringify(state, null, 2), { mode: 0o600 }); fs.renameSync(stateFile + '.tmp', stateFile); fs.unlinkSync(pendingFile); return { ok: true, event: prior ? (data.comparison?.changed ? 'changed' : 'unchanged') : 'baseline_created', url, fetched_at: data.fetched_at, comparison: data.comparison, warnings: data.warnings, settlement: receipt, state_file: stateFile, content_is_untrusted: true }; } finally { fs.closeSync(lock); fs.unlinkSync(lockFile); } } if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { const args = process.argv.slice(2); const networkFlag = args.indexOf('--network'); const network = networkFlag >= 0 ? args.splice(networkFlag, 2)[1] : 'base'; const pay = args.includes('--pay'); const positional = args.filter(arg => arg !== '--pay'); if (positional.length !== 2) { console.error('Usage: node monitor-client.mjs URL baseline.json [--network base|algorand] [--pay]'); process.exitCode = 1; } else runMonitor({ url: positional[0], stateFile: positional[1], network, pay }).then(result => console.log(JSON.stringify(result, null, 2))).catch(error => { console.error(error.message); process.exitCode = 1; }); }