@celorodrigues/x402-conformance 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @celorodrigues/x402-conformance
2
2
 
3
- [![x402-conformance](https://api.automaton-sovereign.workers.dev/v2/badge/default.svg)](https://api.automaton-sovereign.workers.dev/leaderboard)
3
+ [![x402-conformance](https://automaton-api.bfzovw.easypanel.host/v2/badge/default.svg)](https://automaton-api.bfzovw.easypanel.host/leaderboard)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
6
  > **Autonomous Linter and Conformance Suite for Official x402 Micropayments & EIP-3009**
@@ -47,9 +47,34 @@ The same package runs as an MCP stdio server:
47
47
  - Claude Desktop: Settings → Developer → Edit Config (`claude_desktop_config.json`), then restart.
48
48
  - Cursor: `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json`.
49
49
 
50
- Tools: `x402_conformance_check` (free, runs locally), `simulate_base_transaction` (free, EVM dry-run via Base RPC with revert reason decoding), `security_scan` (Base token honeypot / mint-trap / contract risk), `oracle_base`, `sentiment_analysis`, `attest`, `merkle_prove`, plus free `verify_attestation`, `read_ledger`, `merkle_verify`, `pricing`, `health`. Paid tools cost 0.001 - 0.05 USDC on Base (3 free calls/day/IP).
51
-
52
- Optional env: `VALUE_API_BASE` (default `https://api.automaton-sovereign.workers.dev`), `VALUE_API_PAYMENT` (payment tx hash attached to paid calls).
50
+ ### Available MCP Tools (15 Tools)
51
+
52
+ #### 🆓 Free Diagnostics & Local Execution (No payment required)
53
+ | Tool | Description |
54
+ |---|---|
55
+ | `simulate_base_transaction` | EVM transaction dry-run with exact gas estimation and revert reason decoding on Base L2. |
56
+ | `x402_conformance_check` | Full 12-point conformance audit of any HTTP 402 API endpoint against the official spec. |
57
+ | `health` | Real-time service diagnostics, node health, and payment recipient address. |
58
+ | `pricing` | Live pricing directory across all hosted x402 services and settlement rails. |
59
+ | `read_ledger` | Query the immutable, signed ledger of attested operations. |
60
+ | `verify_attestation` | Verify cryptographic signatures and proofs offline or online. |
61
+ | `attestation_pubkey` | Retrieve the agent's public key for independent trustless verification. |
62
+ | `merkle_verify` | Validate cryptographic Merkle tree inclusion proofs. |
63
+
64
+ #### ⚡ Hosted Intelligence & Consensus (x402 Micro-payments on Base — 3 Free Trials/Day/IP)
65
+ | Tool | Price | Description |
66
+ |---|---|---|
67
+ | `oracle_base` | 0.001 USDC | Multi-RPC consensus oracle on Base (block, gas, clock, balance). |
68
+ | `security_scan` | 0.002 USDC | EVM bytecode scanner, honeypot detection, fee-traps & contract risk analysis. |
69
+ | `sentiment_analysis` | 0.001 USDC | Fast agentic financial sentiment scoring for market data. |
70
+ | `attest` | 0.05 USDC | Generate on-chain attestations signed by Agent #95791. |
71
+ | `merkle_prove` | 0.05 USDC | Cryptographic Merkle batch proof generation. |
72
+ | `hash_sha256` | 0.0001 USDC | Fast deterministic SHA-256 computation. |
73
+ | `uuid` | 0.0001 USDC | Cryptographically secure UUID generation. |
74
+
75
+ Optional environment variables:
76
+ - `VALUE_API_BASE`: Default `https://automaton-api.bfzovw.easypanel.host` (or edge mirror `https://api.automaton-sovereign.workers.dev`).
77
+ - `VALUE_API_PAYMENT`: Optional Base USDC transaction hash or EIP-3009 authorization payload.
53
78
 
54
79
  ---
55
80
 
@@ -77,13 +102,13 @@ Optional env: `VALUE_API_BASE` (default `https://api.automaton-sovereign.workers
77
102
  Services that achieve a **CONFORMANT** verdict (Grade A or A+) can request an on-chain signed attestation and permanent verification badge:
78
103
 
79
104
  ```bash
80
- curl -X POST https://api.automaton-sovereign.workers.dev/v2/conformance/certify \
105
+ curl -X POST https://automaton-api.bfzovw.easypanel.host/v2/conformance/certify \
81
106
  -H "Content-Type: application/json" \
82
107
  -H "X-PAYMENT: <txHash-or-EIP3009>" \
83
108
  -d '{"url":"https://your-service.com/v1/paid"}'
84
109
  ```
85
110
 
86
- * Certified services appear on the public [x402 Leaderboard](https://api.automaton-sovereign.workers.dev/leaderboard).
111
+ * Certified services appear on the public [x402 Leaderboard](https://automaton-api.bfzovw.easypanel.host/leaderboard).
87
112
  * Fee: 0.05 USDC settled directly on Base L2 to `0x71DEAc098914A009E3720524642A6bE6F65EE528`.
88
113
 
89
114
  ---
package/mcp-server.js CHANGED
@@ -1,173 +1,202 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- /**
4
- * Automaton-Sovereign Value API — MCP server (stdio transport, zero deps, JSON-RPC 2.0)
5
- *
6
- * Exposes the x402-paid Value API as native MCP tools so any MCP-capable agent
7
- * can call it directly. Paid tools forward the caller's X-PAYMENT tx hash;
8
- * free tools need nothing.
9
- *
10
- * Also exposes the local x402 conformance linter (index.js) as a free tool.
11
- *
12
- * Config via env:
13
- * VALUE_API_BASE base URL of the API (default https://api.automaton-sovereign.workers.dev)
14
- * VALUE_API_PAYMENT default X-PAYMENT tx hash to attach to paid calls (optional)
15
- *
16
- * Usage (Claude Desktop / Cursor MCP config):
17
- * { "mcpServers": { "automaton-x402": { "command": "npx",
18
- * "args": ["-y", "@celorodrigues/x402-conformance", "--mcp"] } } }
19
- */
20
- const http = require('http');
21
- const https = require('https');
22
- const { URL } = require('url');
23
- const PKG = require('./package.json');
24
- const linter = require('./index.js');
25
- const simulator = require('./tx-simulator.js');
26
-
27
- const BASE = (process.env.VALUE_API_BASE || 'https://api.automaton-sovereign.workers.dev').replace(/\/+$/, '');
28
- const PAYMENT = process.env.VALUE_API_PAYMENT || '';
29
-
30
- function httpJson(method, urlStr, headers, body) {
31
- return new Promise((resolve, reject) => {
32
- const u = new URL(urlStr);
33
- const lib = u.protocol === 'http:' ? http : https;
34
- const payload = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : null;
35
- const h = Object.assign({ 'Accept': 'application/json', 'User-Agent': 'automaton-mcp/1.0' }, headers || {});
36
- if (payload) { h['Content-Type'] = 'application/json'; h['Content-Length'] = Buffer.byteLength(payload); }
37
- const req = lib.request({ hostname: u.hostname, port: u.port || (u.protocol === 'http:' ? 80 : 443), path: u.pathname + u.search, method, headers: h }, (res) => {
38
- let d = ''; res.on('data', c => d += c);
39
- res.on('end', () => {
40
- let parsed = null; try { parsed = JSON.parse(d); } catch (e) {}
41
- resolve({ status: res.statusCode, headers: res.headers, body: parsed !== null ? parsed : d });
42
- });
43
- });
44
- req.on('error', reject); req.setTimeout(30000, () => req.destroy(new Error('timeout')));
45
- if (payload) req.write(payload);
46
- req.end();
47
- });
48
- }
49
-
50
- const TOOLS = [
51
- { name: 'x402_conformance_check', description: 'FREE, runs locally. Lint any x402 paid endpoint for protocol conformance: HTTP 402 challenge, x402Version, accepts[] (exact scheme), Base network and addresses, EIP-712 domain, and rejection of malformed, forged, expired, underpaid and wrong-recipient EIP-3009 payments. Returns verdict, grade and per-check results.',
52
- inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Full URL of the x402-protected endpoint to check.' } }, required: ['url'] } },
53
- { name: 'simulate_base_transaction', description: 'FREE, runs locally against Base Mainnet RPC. Dry-run a transaction before sending it: eth_call + eth_estimateGas at the latest block. Predicts whether it will revert and decodes the reason (Error(string), Panic(uint256) codes, custom-error selector, or node rejection such as insufficient funds). Returns { ok, willRevert, revertReason, estimatedGas, returnData }.',
54
- inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Target contract or recipient address on Base (0x...).' }, data: { type: 'string', description: 'Calldata as 0x-hex (default 0x for a plain ETH transfer).' }, value: { type: 'string', description: 'ETH value in wei, decimal or 0x-hex (default 0).' }, from: { type: 'string', description: 'Optional sender address; needed for balance/allowance-dependent calls.' } }, required: ['to'] } },
55
- { name: 'security_scan', description: 'PAID 0.001 USDC/call, or free trial (3/day/IP). Base token safety analysis: honeypot detection, mint traps, selfdestruct/delegatecall, proxy and tax risks from contract bytecode, with a risk score and signed verdict. On 402, pay the accepts[] terms then retry with the payment tx hash.',
56
- inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Token or contract address on Base (0x...).' }, payment: { type: 'string', description: 'Optional X-PAYMENT base tx hash of the USDC payment.' } }, required: ['address'] } },
57
- { name: 'attest', description: 'Create a SIGNED, hash-chained, append-only attestation (verifiable proof-of-existence) for a payload. PAID: 0.001 USDC/call, or free trial (3/day/IP). On 402, pay the accepts[] terms then retry with the payment tx hash.',
58
- inputSchema: { type: 'object', properties: { data: { type: 'string', description: 'Payload to attest (any string).' }, payment: { type: 'string', description: 'Optional X-PAYMENT base tx hash of the USDC payment.' } }, required: ['data'] } },
59
- { name: 'verify_attestation', description: 'FREE. Verify a ledger entry by index or by data hash: recomputes hash, checks ECDSA signature and chain link.',
60
- inputSchema: { type: 'object', properties: { index: { type: 'integer' }, dataHash: { type: 'string' } } } },
61
- { name: 'attestation_pubkey', description: 'FREE. Get the ECDSA P-256 public key (PEM) and keyId used to sign ledger entries.',
62
- inputSchema: { type: 'object', properties: {} } },
63
- { name: 'read_ledger', description: 'FREE. Read the attestation ledger (paginated).',
64
- inputSchema: { type: 'object', properties: { from: { type: 'integer' }, limit: { type: 'integer' } } } },
65
- { name: 'oracle_base', description: 'PAID 0.001 USDC/call. Get real-time Base L2 gas estimates and asset prices (ETH, USDC, cbBTC, AERO, VIRTUAL) with an ECDSA P-256 digital signature from the sovereign agent.',
66
- inputSchema: { type: 'object', properties: { payment: { type: 'string', description: 'Optional X-PAYMENT base tx hash of the USDC payment.' } } } },
67
- { name: 'merkle_prove', description: 'PAID 0.001 USDC/call. Generate a cryptographic Merkle inclusion proof for a list of items and target element.',
68
- inputSchema: { type: 'object', properties: { items: { type: 'array', items: { type: 'string' }, description: 'Array of items' }, target: { type: 'string', description: 'Target item or index' }, payment: { type: 'string' } }, required: ['items'] } },
69
- { name: 'merkle_verify', description: 'FREE. Verify a cryptographic Merkle inclusion proof against a root.',
70
- inputSchema: { type: 'object', properties: { item: { type: 'string' }, proof: { type: 'array', items: { type: 'object' } }, root: { type: 'string' } }, required: ['item', 'proof', 'root'] } },
71
- { name: 'sentiment_analysis', description: 'PAID 0.001 USDC/call. Web3 token risk, security checks, and sentiment scoring with signed verdict.',
72
- inputSchema: { type: 'object', properties: { asset: { type: 'string', description: 'Asset symbol (e.g. ETH, AERO, VIRTUAL)' }, payment: { type: 'string' } }, required: ['asset'] } },
73
- { name: 'hash_sha256', description: 'PAID 0.001 USDC/call. SHA-256 hex of a string.',
74
- inputSchema: { type: 'object', properties: { input: { type: 'string' }, payment: { type: 'string' } }, required: ['input'] } },
75
- { name: 'uuid', description: 'PAID 0.001 USDC/call. Generate a UUIDv4.', inputSchema: { type: 'object', properties: { payment: { type: 'string' } } } },
76
- { name: 'pricing', description: 'FREE. Full pricing terms, endpoints, and payment mechanics.',
77
- inputSchema: { type: 'object', properties: {} } },
78
- { name: 'health', description: 'FREE. Service liveness and version.', inputSchema: { type: 'object', properties: {} } }
79
- ];
80
-
81
- function q(o) { return Object.entries(o || {}).filter(([, v]) => v !== undefined && v !== null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&'); }
82
-
83
- // opts.base / opts.headers let an embedding server (HTTP /mcp) call its own API directly and
84
- // forward the caller's IP / public host.
85
- async function callTool(name, args, opts) {
86
- args = args || {};
87
- opts = opts || {};
88
- const B = (opts.base || BASE).replace(/\/+$/, '');
89
- const fwd = opts.headers || {};
90
- const payHeader = Object.assign({}, fwd, (args.payment || PAYMENT) ? { 'X-PAYMENT': args.payment || PAYMENT } : {});
91
- switch (name) {
92
- case 'x402_conformance_check': {
93
- if (!/^https?:\/\//i.test(args.url || '')) throw new Error('url must be an absolute http(s) URL');
94
- return { status: 200, body: await linter.run(args.url) };
95
- }
96
- case 'simulate_base_transaction': {
97
- const r = await simulator.simulate({ to: args.to, data: args.data, value: args.value, from: args.from });
98
- return { status: r.ok ? 200 : 400, body: r };
99
- }
100
- case 'security_scan': return httpJson('GET', B + '/v2/security/scan?' + q({ address: args.address }), payHeader);
101
- case 'attest': return httpJson('POST', B + '/v2/attest', payHeader, { data: args.data });
102
- case 'verify_attestation': return httpJson('GET', B + '/v2/verify?' + q({ index: args.index, dataHash: args.dataHash }), fwd);
103
- case 'attestation_pubkey': return httpJson('GET', B + '/v2/pubkey', fwd);
104
- case 'read_ledger': return httpJson('GET', B + '/v2/ledger?' + q({ from: args.from, limit: args.limit }), fwd);
105
- case 'oracle_base': return httpJson('GET', B + '/v2/oracle/base', payHeader);
106
- case 'merkle_prove': return httpJson('POST', B + '/v2/merkle/prove', payHeader, { items: args.items, target: args.target });
107
- case 'merkle_verify': return httpJson('POST', B + '/v2/merkle/verify', fwd, { item: args.item, proof: args.proof, root: args.root });
108
- case 'sentiment_analysis': return httpJson('GET', B + '/v2/sentiment?' + q({ asset: args.asset }), payHeader);
109
- case 'hash_sha256': return httpJson('GET', B + '/v1/hash?' + q({ input: args.input }), payHeader);
110
- case 'uuid': return httpJson('GET', B + '/v1/uuid', payHeader);
111
- case 'pricing': return httpJson('GET', B + '/pricing', fwd);
112
- case 'health': return httpJson('GET', B + '/health', fwd);
113
- default: throw new Error('unknown tool: ' + name);
114
- }
115
- }
116
-
117
- const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
118
-
119
- /**
120
- * Handles one JSON-RPC 2.0 message. Resolves to the response object, or null for
121
- * notifications (no id). Shared by the stdio transport and the HTTP /mcp endpoint.
122
- */
123
- async function handleMessage(msg, opts) {
124
- if (!msg || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {
125
- return { jsonrpc: '2.0', id: (msg && msg.id !== undefined) ? msg.id : null, error: { code: -32600, message: 'invalid request' } };
126
- }
127
- const isNotification = msg.id === undefined;
128
- const ok = (result) => isNotification ? null : { jsonrpc: '2.0', id: msg.id, result };
129
- switch (msg.method) {
130
- case 'initialize': {
131
- const asked = msg.params && msg.params.protocolVersion;
132
- return ok({ protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0], capabilities: { tools: {} },
133
- serverInfo: { name: 'automaton-x402', version: PKG.version } });
134
- }
135
- case 'ping': return ok({});
136
- case 'tools/list': return ok({ tools: TOOLS });
137
- case 'tools/call': {
138
- const { name, arguments: a } = msg.params || {};
139
- try {
140
- if (opts && opts.beforeCall) await opts.beforeCall(name, a || {});
141
- const r = await callTool(name, a, opts);
142
- const isErr = r.status >= 400;
143
- // MCP expects content[]; surface status + body, and mark 402 as error with actionable text
144
- const text = typeof r.body === 'string' ? r.body : JSON.stringify(r.body, null, 2);
145
- return ok({ content: [{ type: 'text', text: (isErr ? 'HTTP ' + r.status + '\n' : '') + text }], isError: isErr });
146
- } catch (e) {
147
- return ok({ content: [{ type: 'text', text: 'error: ' + e.message }], isError: true });
148
- }
149
- }
150
- default:
151
- if (msg.method.startsWith('notifications/')) return null;
152
- return isNotification ? null : { jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'method not found: ' + msg.method } };
153
- }
154
- }
155
-
156
- function startStdio() {
157
- let buf = '';
158
- process.stdin.on('data', (chunk) => {
159
- buf += chunk.toString('utf8');
160
- let nl;
161
- while ((nl = buf.indexOf('\n')) >= 0) {
162
- const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
163
- if (!line) continue;
164
- let msg; try { msg = JSON.parse(line); } catch (e) { continue; }
165
- handleMessage(msg).then((r) => { if (r) process.stdout.write(JSON.stringify(r) + '\n'); });
166
- }
167
- });
168
- process.stdin.on('end', () => process.exit(0));
169
- }
170
-
171
- module.exports = { TOOLS, callTool, handleMessage, startStdio };
172
-
173
- if (require.main === module) startStdio();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * Automaton-Sovereign Value API — MCP server (stdio transport, zero deps, JSON-RPC 2.0)
5
+ *
6
+ * Exposes the x402-paid Value API as native MCP tools so any MCP-capable agent
7
+ * can call it directly. Paid tools forward the caller's X-PAYMENT tx hash;
8
+ * free tools need nothing.
9
+ *
10
+ * Also exposes the local x402 conformance linter (index.js) as a free tool.
11
+ *
12
+ * Config via env:
13
+ * VALUE_API_BASE base URL of the API (default https://api.automaton-sovereign.workers.dev)
14
+ * VALUE_API_PAYMENT default X-PAYMENT tx hash to attach to paid calls (optional)
15
+ *
16
+ * Usage (Claude Desktop / Cursor MCP config):
17
+ * { "mcpServers": { "automaton-x402": { "command": "npx",
18
+ * "args": ["-y", "@celorodrigues/x402-conformance", "--mcp"] } } }
19
+ */
20
+ const http = require('http');
21
+ const https = require('https');
22
+ const { URL } = require('url');
23
+ const PKG = require('./package.json');
24
+ const linter = require('./index.js');
25
+ const simulator = require('./tx-simulator.js');
26
+
27
+ const BASE = (process.env.VALUE_API_BASE || 'https://automaton-api.bfzovw.easypanel.host').replace(/\/+$/, '');
28
+ const PAYMENT = process.env.VALUE_API_PAYMENT || '';
29
+
30
+ function httpJson(method, urlStr, headers, body) {
31
+ return new Promise((resolve, reject) => {
32
+ const u = new URL(urlStr);
33
+ const lib = u.protocol === 'http:' ? http : https;
34
+ const payload = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : null;
35
+ const h = Object.assign({ 'Accept': 'application/json', 'User-Agent': 'automaton-mcp/1.0' }, headers || {});
36
+ if (payload) { h['Content-Type'] = 'application/json'; h['Content-Length'] = Buffer.byteLength(payload); }
37
+ const req = lib.request({ hostname: u.hostname, port: u.port || (u.protocol === 'http:' ? 80 : 443), path: u.pathname + u.search, method, headers: h }, (res) => {
38
+ let d = ''; res.on('data', c => d += c);
39
+ res.on('end', () => {
40
+ let parsed = null; try { parsed = JSON.parse(d); } catch (e) {}
41
+ resolve({ status: res.statusCode, headers: res.headers, body: parsed !== null ? parsed : d });
42
+ });
43
+ });
44
+ req.on('error', reject); req.setTimeout(30000, () => req.destroy(new Error('timeout')));
45
+ if (payload) req.write(payload);
46
+ req.end();
47
+ });
48
+ }
49
+
50
+ const TOOLS = [
51
+ { name: 'x402_conformance_check', description: 'FREE, runs locally. Lint any x402 paid endpoint for protocol conformance: HTTP 402 challenge, x402Version, accepts[] (exact scheme), Base network and addresses, EIP-712 domain, and rejection of malformed, forged, expired, underpaid and wrong-recipient EIP-3009 payments. Returns verdict, grade and per-check results.',
52
+ inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'Full URL of the x402-protected endpoint to check.' } }, required: ['url'] } },
53
+ { name: 'simulate_base_transaction', description: 'FREE, runs locally against Base Mainnet RPC. Dry-run a transaction before sending it: eth_call + eth_estimateGas at the latest block. Predicts whether it will revert and decodes the reason (Error(string), Panic(uint256) codes, custom-error selector, or node rejection such as insufficient funds). Returns { ok, willRevert, revertReason, estimatedGas, returnData }.',
54
+ inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Target contract or recipient address on Base (0x...).' }, data: { type: 'string', description: 'Calldata as 0x-hex (default 0x for a plain ETH transfer).' }, value: { type: 'string', description: 'ETH value in wei, decimal or 0x-hex (default 0).' }, from: { type: 'string', description: 'Optional sender address; needed for balance/allowance-dependent calls.' } }, required: ['to'] } },
55
+ // ---- Token Safety suite --------------------------------------------------------------
56
+ // Names match mcp-bundle/ (which mirrors the published plugin's schemas.ts), so one client
57
+ // can use the same names against either server.
58
+ { name: 'token_scan', description: 'PAID 0.001 USDC/call, or free trial (3/day/IP). Bytecode security scan of a Base token contract: honeypot opcodes, mint/blacklist/pause/tax capabilities, a risk score and a verdict. Read-only — no transaction is sent. On 402, pay the accepts[] terms then retry with the payment tx hash. (Exposed as `security_scan` on older clients.)',
59
+ inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Token contract on Base (0x...)' }, payment: { type: 'string', description: 'Optional X-PAYMENT base tx hash of the USDC payment.' } }, required: ['address'] } },
60
+ { name: 'tx_simulate', description: 'PAID 0.01 USDC/call, or free trial (3/day/IP). Read-only buy/sell simulation of a Base token through the Uniswap V2 WETH pool: returns reverts, the MEASURED transfer tax and a honeypot verdict. No transaction is sent. On 402, pay the accepts[] terms then retry with the payment tx hash. (Not the same tool as simulate_base_transaction, which dry-runs arbitrary calldata for free.)',
61
+ inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'Token contract to trade (0x...)' }, side: { type: 'string', enum: ['buy', 'sell'], description: 'Trade direction being simulated' }, amount: { type: 'string', description: 'ETH for a buy, token units for a sell (positive decimal string)' }, wallet: { type: 'string', description: 'Address simulated as the trader (0x...)' }, payment: { type: 'string' } }, required: ['token', 'side', 'amount', 'wallet'] } },
62
+ { name: 'approval_risk', description: 'PAID 0.01 USDC/call, or free trial (3/day/IP). Read-only audit of the ERC-20 approvals an owner granted for a token: spender, allowance, contract vs EOA, verified flag, drainable amount and a drain-risk verdict. On 402, pay the accepts[] terms then retry with the payment tx hash.',
63
+ inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'ERC-20 token contract (0x...)' }, owner: { type: 'string', description: 'Address whose approvals are audited (0x...)' }, spenders: { type: 'array', items: { type: 'string' }, maxItems: 20, description: 'Extra spenders to check beyond the log window' }, payment: { type: 'string' } }, required: ['token', 'owner'] } },
64
+ { name: 'liquidity_risk', description: 'PAID 0.01 USDC/call, or free trial (3/day/IP). One-block liquidity audit of a Base token: Uniswap V3 / Aerodrome pools, the USD needed to move the price 1/2/5/10%, holder concentration, LP burn evidence and explicit coverage gaps. On 402, pay the accepts[] terms then retry with the payment tx hash.',
65
+ inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'Token contract to audit (0x...)' }, pair: { type: 'string', description: 'Analyze only this pool instead of all pools (0x...)' }, payment: { type: 'string' } }, required: ['token'] } },
66
+ { name: 'sentinel_latest', description: 'PAID 0.001 USDC/call, or free trial (3/day/IP). Newest liquidity pools created on Base (Uniswap v3/v4, Aerodrome) with a bytecode risk score per new token. Filters are optional. On 402, pay the accepts[] terms then retry with the payment tx hash.',
67
+ inputSchema: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 500, description: 'Max pools to return (default 50)' }, maxRisk: { type: 'number', minimum: 0, maximum: 100, description: 'Only pools whose risk score is at most this' }, minRisk: { type: 'number', minimum: 0, maximum: 100, description: 'Only pools whose risk score is at least this' }, dex: { type: 'string', enum: ['uniswap-v4', 'uniswap-v3', 'aerodrome', 'aerodrome-slipstream'], description: 'Restrict to one venue' }, since: { type: 'string', description: 'ISO-8601 timestamp; only pools detected after it' }, payment: { type: 'string' } }, required: [] } },
68
+ // NOTE: sentinel_stream is deliberately NOT exposed here. It is an SSE channel that has to be
69
+ // held open for a window, and this server answers one buffered request per call (handleMessage
70
+ // returns a single result, and mcp-http-server buffers the body). Adding it would mean either
71
+ // blocking the caller for the whole window or lying about what arrived. mcp-bundle exposes it
72
+ // properly, with a bounded window and an explicit report of how long it listened.
73
+ { name: 'security_scan', description: 'LEGACY NAME of token_scan — same route, same arguments, kept so clients written against this server keep working. PAID 0.001 USDC/call, or free trial (3/day/IP). Base token safety analysis: honeypot detection, mint traps, selfdestruct/delegatecall, proxy and tax risks from contract bytecode, with a risk score and signed verdict. On 402, pay the accepts[] terms then retry with the payment tx hash.',
74
+ inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Token or contract address on Base (0x...).' }, payment: { type: 'string', description: 'Optional X-PAYMENT base tx hash of the USDC payment.' } }, required: ['address'] } },
75
+ { name: 'attest', description: 'Create a SIGNED, hash-chained, append-only attestation (verifiable proof-of-existence) for a payload. PAID: 0.05 USDC/call, or free trial (3/day/IP). On 402, pay the accepts[] terms then retry with the payment tx hash.',
76
+ inputSchema: { type: 'object', properties: { data: { type: 'string', description: 'Payload to attest (any string).' }, payment: { type: 'string', description: 'Optional X-PAYMENT base tx hash of the USDC payment.' } }, required: ['data'] } },
77
+ { name: 'verify_attestation', description: 'FREE. Verify a ledger entry by index or by data hash: recomputes hash, checks ECDSA signature and chain link.',
78
+ inputSchema: { type: 'object', properties: { index: { type: 'integer' }, dataHash: { type: 'string' } } } },
79
+ { name: 'attestation_pubkey', description: 'FREE. Get the ECDSA P-256 public key (PEM) and keyId used to sign ledger entries.',
80
+ inputSchema: { type: 'object', properties: {} } },
81
+ { name: 'read_ledger', description: 'FREE. Read the attestation ledger (paginated).',
82
+ inputSchema: { type: 'object', properties: { from: { type: 'integer' }, limit: { type: 'integer' } } } },
83
+ { name: 'oracle_base', description: 'PAID 0.001 USDC/call. Get real-time Base L2 gas estimates and asset prices (ETH, USDC, cbBTC, AERO, VIRTUAL) with an ECDSA P-256 digital signature from the sovereign agent.',
84
+ inputSchema: { type: 'object', properties: { payment: { type: 'string', description: 'Optional X-PAYMENT base tx hash of the USDC payment.' } } } },
85
+ { name: 'merkle_prove', description: 'PAID 0.001 USDC/call. Generate a cryptographic Merkle inclusion proof for a list of items and target element.',
86
+ inputSchema: { type: 'object', properties: { items: { type: 'array', items: { type: 'string' }, description: 'Array of items' }, target: { type: 'string', description: 'Target item or index' }, payment: { type: 'string' } }, required: ['items'] } },
87
+ { name: 'merkle_verify', description: 'FREE. Verify a cryptographic Merkle inclusion proof against a root.',
88
+ inputSchema: { type: 'object', properties: { item: { type: 'string' }, proof: { type: 'array', items: { type: 'object' } }, root: { type: 'string' } }, required: ['item', 'proof', 'root'] } },
89
+ { name: 'sentiment_analysis', description: 'PAID 0.001 USDC/call. Web3 token risk, security checks, and sentiment scoring with signed verdict.',
90
+ inputSchema: { type: 'object', properties: { asset: { type: 'string', description: 'Asset symbol (e.g. ETH, AERO, VIRTUAL)' }, payment: { type: 'string' } }, required: ['asset'] } },
91
+ { name: 'hash_sha256', description: 'PAID 0.001 USDC/call. SHA-256 hex of a string.',
92
+ inputSchema: { type: 'object', properties: { input: { type: 'string' }, payment: { type: 'string' } }, required: ['input'] } },
93
+ { name: 'uuid', description: 'PAID 0.001 USDC/call. Generate a UUIDv4.', inputSchema: { type: 'object', properties: { payment: { type: 'string' } } } },
94
+ { name: 'pricing', description: 'FREE. Full pricing terms, endpoints, and payment mechanics.',
95
+ inputSchema: { type: 'object', properties: {} } },
96
+ { name: 'health', description: 'FREE. Service liveness and version.', inputSchema: { type: 'object', properties: {} } }
97
+ ];
98
+
99
+ function q(o) { return Object.entries(o || {}).filter(([, v]) => v !== undefined && v !== null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&'); }
100
+
101
+ // opts.base / opts.headers let an embedding server (HTTP /mcp) call its own API directly and
102
+ // forward the caller's IP / public host.
103
+ async function callTool(name, args, opts) {
104
+ args = args || {};
105
+ opts = opts || {};
106
+ const B = (opts.base || BASE).replace(/\/+$/, '');
107
+ const fwd = opts.headers || {};
108
+ const payHeader = Object.assign({}, fwd, (args.payment || PAYMENT) ? { 'X-PAYMENT': args.payment || PAYMENT } : {});
109
+ switch (name) {
110
+ case 'x402_conformance_check': {
111
+ if (!/^https?:\/\//i.test(args.url || '')) throw new Error('url must be an absolute http(s) URL');
112
+ return { status: 200, body: await linter.run(args.url) };
113
+ }
114
+ case 'simulate_base_transaction': {
115
+ const r = await simulator.simulate({ to: args.to, data: args.data, value: args.value, from: args.from });
116
+ return { status: r.ok ? 200 : 400, body: r };
117
+ }
118
+ // ---- Token Safety suite: same routes the mcp-bundle tools call, same required fields.
119
+ // `token_scan` and `security_scan` are two names for one route on purpose: the second is the
120
+ // legacy name this server has always published, the first is what mcp-bundle exposes.
121
+ case 'token_scan':
122
+ case 'security_scan': return httpJson('GET', B + '/v2/security/scan?' + q({ address: args.address }), payHeader);
123
+ case 'tx_simulate': return httpJson('POST', B + '/v2/token/simulate', payHeader,
124
+ { token: args.token, side: args.side, amount: args.amount, wallet: args.wallet });
125
+ case 'approval_risk': return httpJson('POST', B + '/v2/token/approval-risk', payHeader,
126
+ args.spenders !== undefined ? { token: args.token, owner: args.owner, spenders: args.spenders } : { token: args.token, owner: args.owner });
127
+ case 'liquidity_risk': return httpJson('POST', B + '/v2/token/liquidity-risk', payHeader,
128
+ args.pair !== undefined ? { token: args.token, pair: args.pair } : { token: args.token });
129
+ case 'sentinel_latest': return httpJson('GET', B + '/v2/sentinel/latest?' + q({ limit: args.limit, maxRisk: args.maxRisk, minRisk: args.minRisk, dex: args.dex, since: args.since }), payHeader);
130
+ case 'attest': return httpJson('POST', B + '/v2/attest', payHeader, { data: args.data });
131
+ case 'verify_attestation': return httpJson('GET', B + '/v2/verify?' + q({ index: args.index, dataHash: args.dataHash }), fwd);
132
+ case 'attestation_pubkey': return httpJson('GET', B + '/v2/pubkey', fwd);
133
+ case 'read_ledger': return httpJson('GET', B + '/v2/ledger?' + q({ from: args.from, limit: args.limit }), fwd);
134
+ case 'oracle_base': return httpJson('GET', B + '/v2/oracle/base', payHeader);
135
+ case 'merkle_prove': return httpJson('POST', B + '/v2/merkle/prove', payHeader, { items: args.items, target: args.target });
136
+ case 'merkle_verify': return httpJson('POST', B + '/v2/merkle/verify', fwd, { item: args.item, proof: args.proof, root: args.root });
137
+ case 'sentiment_analysis': return httpJson('GET', B + '/v2/sentiment?' + q({ asset: args.asset }), payHeader);
138
+ case 'hash_sha256': return httpJson('GET', B + '/v1/hash?' + q({ input: args.input }), payHeader);
139
+ case 'uuid': return httpJson('GET', B + '/v1/uuid', payHeader);
140
+ case 'pricing': return httpJson('GET', B + '/pricing', fwd);
141
+ case 'health': return httpJson('GET', B + '/health', fwd);
142
+ default: throw new Error('unknown tool: ' + name);
143
+ }
144
+ }
145
+
146
+ const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
147
+
148
+ /**
149
+ * Handles one JSON-RPC 2.0 message. Resolves to the response object, or null for
150
+ * notifications (no id). Shared by the stdio transport and the HTTP /mcp endpoint.
151
+ */
152
+ async function handleMessage(msg, opts) {
153
+ if (!msg || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {
154
+ return { jsonrpc: '2.0', id: (msg && msg.id !== undefined) ? msg.id : null, error: { code: -32600, message: 'invalid request' } };
155
+ }
156
+ const isNotification = msg.id === undefined;
157
+ const ok = (result) => isNotification ? null : { jsonrpc: '2.0', id: msg.id, result };
158
+ switch (msg.method) {
159
+ case 'initialize': {
160
+ const asked = msg.params && msg.params.protocolVersion;
161
+ return ok({ protocolVersion: PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0], capabilities: { tools: {} },
162
+ serverInfo: { name: 'automaton-x402', version: PKG.version } });
163
+ }
164
+ case 'ping': return ok({});
165
+ case 'tools/list': return ok({ tools: TOOLS });
166
+ case 'tools/call': {
167
+ const { name, arguments: a } = msg.params || {};
168
+ try {
169
+ if (opts && opts.beforeCall) await opts.beforeCall(name, a || {});
170
+ const r = await callTool(name, a, opts);
171
+ const isErr = r.status >= 400;
172
+ // MCP expects content[]; surface status + body, and mark 402 as error with actionable text
173
+ const text = typeof r.body === 'string' ? r.body : JSON.stringify(r.body, null, 2);
174
+ return ok({ content: [{ type: 'text', text: (isErr ? 'HTTP ' + r.status + '\n' : '') + text }], isError: isErr });
175
+ } catch (e) {
176
+ return ok({ content: [{ type: 'text', text: 'error: ' + e.message }], isError: true });
177
+ }
178
+ }
179
+ default:
180
+ if (msg.method.startsWith('notifications/')) return null;
181
+ return isNotification ? null : { jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'method not found: ' + msg.method } };
182
+ }
183
+ }
184
+
185
+ function startStdio() {
186
+ let buf = '';
187
+ process.stdin.on('data', (chunk) => {
188
+ buf += chunk.toString('utf8');
189
+ let nl;
190
+ while ((nl = buf.indexOf('\n')) >= 0) {
191
+ const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
192
+ if (!line) continue;
193
+ let msg; try { msg = JSON.parse(line); } catch (e) { continue; }
194
+ handleMessage(msg).then((r) => { if (r) process.stdout.write(JSON.stringify(r) + '\n'); });
195
+ }
196
+ });
197
+ process.stdin.on('end', () => process.exit(0));
198
+ }
199
+
200
+ module.exports = { TOOLS, callTool, handleMessage, startStdio };
201
+
202
+ if (require.main === module) startStdio();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celorodrigues/x402-conformance",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Zero-dependency CLI, MCP server and testing suite for x402 AI agent micropayment protocol & EIP-3009 compliance",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -38,7 +38,7 @@
38
38
  "type": "git",
39
39
  "url": "git+https://github.com/baianomarceloeduardo-jpg/automaton-x402.git"
40
40
  },
41
- "homepage": "https://api.automaton-sovereign.workers.dev/leaderboard",
41
+ "homepage": "https://automaton-api.bfzovw.easypanel.host/leaderboard",
42
42
  "bugs": {
43
43
  "url": "https://github.com/baianomarceloeduardo-jpg/automaton-x402/issues"
44
44
  },
package/smithery.yaml CHANGED
@@ -7,20 +7,20 @@ startCommand:
7
7
  properties:
8
8
  valueApiBase:
9
9
  type: string
10
- default: https://api.automaton-sovereign.workers.dev
10
+ default: https://automaton-api.bfzovw.easypanel.host
11
11
  description: Base URL of the Automaton-Sovereign Value API used by the paid/free API tools.
12
12
  valueApiPayment:
13
13
  type: string
14
- description: Optional x402 payment (Base USDC tx hash) attached as X-PAYMENT to paid tool calls.
14
+ description: Optional x402 payment (Base USDC tx hash or EIP-3009 authorization) attached to paid tool calls.
15
15
  commandFunction:
16
16
  |-
17
17
  (config) => ({
18
18
  command: 'npx',
19
- args: ['-y', '@celorodrigues/x402-conformance', '--mcp'],
19
+ args: ['-y', '@celorodrigues/x402-conformance@2.2.0', '--mcp'],
20
20
  env: Object.assign(
21
- { VALUE_API_BASE: config.valueApiBase || 'https://api.automaton-sovereign.workers.dev' },
21
+ { VALUE_API_BASE: config.valueApiBase || 'https://automaton-api.bfzovw.easypanel.host' },
22
22
  config.valueApiPayment ? { VALUE_API_PAYMENT: config.valueApiPayment } : {}
23
23
  )
24
24
  })
25
25
  exampleConfig:
26
- valueApiBase: https://api.automaton-sovereign.workers.dev
26
+ valueApiBase: https://automaton-api.bfzovw.easypanel.host
package/tx-simulator.js CHANGED
@@ -14,7 +14,10 @@ const https = require('https');
14
14
  const http = require('http');
15
15
  const { URL } = require('url');
16
16
 
17
- const DEFAULT_RPC = process.env.BASE_RPC_URL || 'https://mainnet.base.org';
17
+ const DEFAULT_RPC = process.env.SIM_RPC_URL || process.env.BASE_RPC_URL || 'https://mainnet.base.org';
18
+ const MAX_DATA_BYTES = 65536; // well above any real tx; bounds upstream work
19
+ const MAX_DATA_HEX = 2 + MAX_DATA_BYTES * 2;
20
+ const MAX_RPC_RESPONSE = 2 * 1024 * 1024; // cap memory per upstream response
18
21
  const BASE_CHAIN_ID = '0x2105'; // 8453
19
22
 
20
23
  const PANIC_CODES = {
@@ -37,7 +40,8 @@ function rpcCall(rpcUrl, method, params) {
37
40
  const payload = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
38
41
  const req = lib.request({ hostname: u.hostname, port: u.port || (u.protocol === 'http:' ? 80 : 443), path: u.pathname + u.search, method: 'POST',
39
42
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), 'User-Agent': 'automaton-simulator/1.0' } }, (res) => {
40
- let d = ''; res.on('data', (c) => d += c);
43
+ let d = '';
44
+ res.on('data', (c) => { d += c; if (d.length > MAX_RPC_RESPONSE) req.destroy(new Error('rpc_response_too_large')); });
41
45
  res.on('end', () => {
42
46
  let j; try { j = JSON.parse(d); } catch (e) { return reject(new Error('bad_rpc_response (HTTP ' + res.statusCode + ')')); }
43
47
  resolve(j); // caller inspects j.result / j.error (a revert arrives as j.error)
@@ -94,17 +98,64 @@ function revertDataOf(err) {
94
98
  return null;
95
99
  }
96
100
 
101
+ // JSON-RPC error -> 'revert' (execution reverted), 'rejected' (node refuses the tx itself:
102
+ // funds, gas, nonce) or 'transient' (rate limit, capacity, internal: says nothing about the tx).
103
+ function classify(err) {
104
+ if (!err) return null;
105
+ if (revertDataOf(err)) return 'revert';
106
+ const msg = String(err.message || '');
107
+ if (err.code === 3 || /revert/i.test(msg)) return 'revert';
108
+ if (/insufficient funds|OutOfFunds|gas required exceeds|intrinsic gas|gas too low|exceeds block gas limit|nonce|invalid opcode|out of gas|stack (over|under)flow|InvalidFEOpcode|OpcodeNotFound/i.test(msg)) return 'rejected';
109
+ return 'transient';
110
+ }
111
+
112
+ // Returns an error string for unusable input, or null. Pure and cheap: callers run it before
113
+ // charging a payment or spending an RPC call.
114
+ function validate(input) {
115
+ input = input || {};
116
+ if (!isAddr(input.to)) return 'to must be a 0x-prefixed 20-byte address';
117
+ if (input.from !== undefined && input.from !== null && input.from !== '' && !isAddr(input.from)) return 'from must be a 0x-prefixed 20-byte address';
118
+ const data = input.data === undefined || input.data === null || input.data === '' ? '0x' : input.data;
119
+ if (typeof data !== 'string') return 'data must be a 0x-hex string';
120
+ if (data.length > MAX_DATA_HEX) return 'calldata too large (max ' + MAX_DATA_BYTES + ' bytes)';
121
+ if (!/^0x([0-9a-fA-F]{2})*$/.test(data)) return 'data must be 0x-prefixed even-length hex';
122
+ try { toQuantity(input.value); } catch (e) { return e.message; }
123
+ return null;
124
+ }
125
+
126
+ // Process-wide limiter: the free MCP tool must not turn this host into an open Base RPC proxy
127
+ // (each simulation costs 4 upstream calls, and the same public RPC verifies our payments).
128
+ const RATE_PER_MIN = parseInt(process.env.SIM_RATE_PER_MIN || '120', 10);
129
+ const MAX_CONCURRENT = parseInt(process.env.SIM_MAX_CONCURRENT || '8', 10);
130
+ let tokens = RATE_PER_MIN, refillAt = Date.now(), inFlight = 0;
131
+ function acquire() {
132
+ const now = Date.now();
133
+ tokens = Math.min(RATE_PER_MIN, tokens + ((now - refillAt) / 60000) * RATE_PER_MIN);
134
+ refillAt = now;
135
+ if (inFlight >= MAX_CONCURRENT) return 'rate_limited: too many concurrent simulations, retry shortly';
136
+ if (tokens < 1) return 'rate_limited: simulation quota exceeded (' + RATE_PER_MIN + '/min), retry shortly';
137
+ tokens -= 1; inFlight++;
138
+ return null;
139
+ }
140
+
97
141
  async function simulate(input, opts) {
98
142
  input = input || {};
99
143
  opts = opts || {};
100
- const rpcUrl = opts.rpcUrl || DEFAULT_RPC;
101
144
  const out = { ok: false, network: 'base', chainId: 8453, willRevert: null, revertReason: null, estimatedGas: null, returnData: null };
145
+ const invalid = validate(input);
146
+ if (invalid) { out.error = invalid; return out; }
147
+ // Paid HTTP calls (already metered by x402) skip the free-tier limiter but not the concurrency cap.
148
+ if (!opts.bypassLimit) {
149
+ const limited = acquire();
150
+ if (limited) { out.error = limited; out.rateLimited = true; return out; }
151
+ } else inFlight++;
152
+ try { return await run(input, opts, out); } finally { inFlight--; }
153
+ }
102
154
 
103
- if (!isAddr(input.to)) { out.error = 'to must be a 0x-prefixed 20-byte address'; return out; }
104
- if (input.from !== undefined && input.from !== null && input.from !== '' && !isAddr(input.from)) { out.error = 'from must be a 0x-prefixed 20-byte address'; return out; }
105
- const data = input.data === undefined || input.data === null || input.data === '' ? '0x' : String(input.data);
106
- if (!/^0x([0-9a-fA-F]{2})*$/.test(data)) { out.error = 'data must be 0x-prefixed even-length hex'; return out; }
107
- let value; try { value = toQuantity(input.value); } catch (e) { out.error = e.message; return out; }
155
+ async function run(input, opts, out) {
156
+ const rpcUrl = opts.rpcUrl || DEFAULT_RPC;
157
+ const data = input.data === undefined || input.data === null || input.data === '' ? '0x' : input.data;
158
+ const value = toQuantity(input.value);
108
159
 
109
160
  const tx = { to: input.to, data, value };
110
161
  if (input.from) tx.from = input.from;
@@ -120,9 +171,18 @@ async function simulate(input, opts) {
120
171
  out.tx = tx;
121
172
 
122
173
  let call, gas;
123
- try {
124
- [call, gas] = await Promise.all([rpcCall(rpcUrl, 'eth_call', [tx, block]), rpcCall(rpcUrl, 'eth_estimateGas', [tx, block])]);
125
- } catch (e) { out.error = 'rpc_unreachable: ' + e.message; return out; }
174
+ for (let attempt = 0; attempt < 2; attempt++) {
175
+ try {
176
+ [call, gas] = await Promise.all([rpcCall(rpcUrl, 'eth_call', [tx, block]), rpcCall(rpcUrl, 'eth_estimateGas', [tx, block])]);
177
+ } catch (e) { out.error = 'rpc_unreachable: ' + e.message; return out; }
178
+ // Rate limits / capacity errors are not reverts: retry once, then report ok:false.
179
+ if (classify(call.error) !== 'transient' && classify(gas.error) !== 'transient') break;
180
+ if (attempt === 0) await new Promise((r) => setTimeout(r, 400));
181
+ }
182
+ if (classify(call.error) === 'transient') {
183
+ out.error = 'rpc_error: ' + String(call.error.message || call.error.code || 'unknown').slice(0, 200);
184
+ return out;
185
+ }
126
186
 
127
187
  out.ok = true;
128
188
  if (call.error) {
@@ -149,7 +209,8 @@ async function simulate(input, opts) {
149
209
  else if (gas.error) {
150
210
  out.estimateGasError = String(gas.error.message || 'estimateGas failed');
151
211
  // eth_call can pass while estimateGas fails (e.g. gas-dependent logic): still a revert on-chain.
152
- if (out.willRevert === false) {
212
+ // A transient RPC error on estimateGas alone says nothing about the tx, so it does not flip it.
213
+ if (out.willRevert === false && classify(gas.error) !== 'transient') {
153
214
  out.willRevert = true;
154
215
  const rd = revertDataOf(gas.error);
155
216
  const dec = rd ? decodeRevert(rd) : null;
@@ -162,4 +223,4 @@ async function simulate(input, opts) {
162
223
  return out;
163
224
  }
164
225
 
165
- module.exports = { simulate, decodeRevert, toQuantity, PANIC_CODES };
226
+ module.exports = { simulate, validate, decodeRevert, toQuantity, PANIC_CODES };