aere-quantum/agents/x402/resource-server.mjs
Aere Network 2293c1da86 agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet
The agent does not hold the payment key: the wallet holds it for the owner and signs an EIP-3009 authorization only for a payment the
agent wrote into its signed ledger, verified without trusting the agent under the policy the owner pinned and against the ledger heads
the wallet itself saw (a branch is refused with a proof of equivocation, a backdated entry is refused), naming exactly this purchase,
written now, and within the limit judged also against what the wallet itself has signed. The authorization nonce is sha256(entry hash),
so the on-chain payment names the ledger entry. The policy gains an optional `wallet` field. Also: an x402 v2 client, a minimal resource
server, a local facilitator for tests, and verifica-plati.mjs, which proves from outside that a wallet's on-chain payments were allowed
by the agent's policy (with --all-transfers, that no payment left the wallet without a ledger entry).

Tests: wallet 25/25 and payment verifier 10/10 without a network (the verifier on chain responses recorded on testnet 28001), negative
control 21/21; policy 27/27, agents control 26/26. On the public testnet 28001 through its x402 facilitator: 9/9, with the evidence in
agents/x402/dovezi-28001/. Needs ethers (npm install in agents/x402).
2026-09-30 00:01:34 +03:00

40 lines
3.1 KiB
JavaScript

// Un server de resurse x402 v2, minimal (2026-09-29): raspunde 402 cu PAYMENT-REQUIRED, iar la reluare trimite plata facilitatorului
// (POST /verify, apoi POST /settle) si serveste resursa numai dupa o decontare reusita, cu PAYMENT-RESPONSE. E piesa pe care o are
// orice vanzator x402; aici e ca probele agentilor sa plateasca cap la cap printr-un facilitator adevarat (cel de pe testnetul 28001)
// sau printr-unul local. Nu are voie sa accepte o plata pentru alta cerinta decat a emis-o: `accepted` trebuie sa fie chiar ea.
import http from 'node:http';
import { b64json, dinB64json } from './client.mjs';
/**
* @param {object} o { requirement:{scheme,network,amount,asset,payTo,maxTimeoutSeconds,extra}, facilitator: URL, path, content, description }
*/
export function createResourceServer({ requirement, facilitator, path: cale = '/premium', content = 'the paid content', description = 'a paid resource', fetchImpl = fetch }) {
const fac = String(facilitator).replace(/\/+$/, '');
const post = async (p, body) => { const r = await fetchImpl(fac + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }); return r.json(); };
const cheie = (x) => JSON.stringify(['scheme', 'network', 'amount', 'asset', 'payTo', 'maxTimeoutSeconds'].map((k) => String(x && x[k]).toLowerCase()));
const decontari = [], primite = [];
const srv = http.createServer(async (req, res) => {
const url = (req.url || '/').split('?')[0];
if (url !== cale) { res.writeHead(404); return res.end('not found'); }
const resource = { url: `http://${req.headers.host}${cale}`, description, mimeType: 'text/plain' };
const cere = (error) => { const pr = { x402Version: 2, error, resource, accepts: [requirement] };
res.writeHead(402, { 'content-type': 'application/json', 'PAYMENT-REQUIRED': b64json(pr) }); res.end(JSON.stringify(pr)); };
const h = req.headers['payment-signature'];
if (!h) return cere('payment required');
const payload = dinB64json(h);
if (!payload || payload.x402Version !== 2 || !payload.payload) return cere('the PAYMENT-SIGNATURE header is not an x402 v2 payment payload');
if (cheie(payload.accepted) !== cheie(requirement)) return cere('the payment is for another requirement than this resource issued');
primite.push(payload);
try {
const body = { x402Version: 2, paymentPayload: payload, paymentRequirements: requirement };
const v = await post('/verify', body);
if (!v.isValid) return cere(`payment not valid: ${v.invalidReason}`);
const s = await post('/settle', body);
decontari.push(s);
if (!s.success) return cere(`settlement failed: ${String(s.errorReason).slice(0, 120)}`);
res.writeHead(200, { 'content-type': 'text/plain', 'PAYMENT-RESPONSE': b64json(s) }); res.end(content);
} catch (e) { res.writeHead(502, { 'content-type': 'text/plain' }); res.end('facilitator unreachable: ' + String(e.message).slice(0, 80)); }
});
return { server: srv, settlements: decontari, lastPayloads: primite, listen: (port = 0, host = '127.0.0.1') => new Promise((r) => srv.listen(port, host, () => r(`http://${host}:${srv.address().port}${cale}`))) };
}