@celorodrigues/x402-conformance 2.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Automaton-Sovereign
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @celorodrigues/x402-conformance
2
+
3
+ [![x402-conformance](https://api.automaton-sovereign.workers.dev/v2/badge/default.svg)](https://api.automaton-sovereign.workers.dev/leaderboard)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ > **Autonomous Linter and Conformance Suite for Official x402 Micropayments & EIP-3009**
7
+
8
+ Zero-dependency CLI tool and auditing engine to verify that an AI agent API strictly complies with the official Coinbase x402 specification (v1 & v2 CAIP-2) and EIP-3009 gasless authorization mechanics on Base L2.
9
+
10
+ ---
11
+
12
+ ## ⚡ Quickstart
13
+
14
+ Run directly without installing via `npx`:
15
+
16
+ ```bash
17
+ npx @celorodrigues/x402-conformance https://your-service.com/v1/paid
18
+ ```
19
+
20
+ ### Machine-readable JSON:
21
+ ```bash
22
+ npx @celorodrigues/x402-conformance https://your-service.com/v1/paid --json
23
+ ```
24
+
25
+ ### Generate SVG Badge:
26
+ ```bash
27
+ npx @celorodrigues/x402-conformance https://your-service.com/v1/paid --svg > badge.svg
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 🧪 Battery of Checks
33
+
34
+ | Code | Level | Check | Requirement |
35
+ |---|---|---|---|
36
+ | **S1** | MUST | HTTP 402 Challenge | Unpaid request must return HTTP status 402 |
37
+ | **S2** | MUST | x402Version Spec | Challenge body must define valid integer x402Version |
38
+ | **S3** | MUST | `accepts[]` Structure | Contains non-empty accepts[] with `scheme=exact` |
39
+ | **S4** | MUST | Network & Address | Valid Base network and checksummed recipient addresses |
40
+ | **S5** | MUST | EIP-712 Domain Extra | `extra.name="USD Coin"` and `extra.version="2"` present |
41
+ | **S6** | SHOULD | Absolute Resource URI | `resource` must be a valid absolute URI |
42
+ | **S7** | MUST | Malformed Payment Rejection | Garbage `X-PAYMENT` header must return 400/402, never 200 |
43
+ | **S8** | MUST | Tampered Signature Rejection | Corrupted EIP-3009 signature must fail closed |
44
+ | **S9** | MUST | Expired Authorization Rejection | Expired `validBefore` timestamp must be rejected |
45
+ | **S10** | MUST | Underpaid Value Rejection | Amounts below `maxAmountRequired` must be rejected |
46
+ | **S11** | MUST | Wrong Recipient Rejection | Transfer authorizations directed to other addresses rejected |
47
+ | **S12** | INFO | Discovery Surface | Discovery endpoints (`/.well-known/x402`, OpenAPI) detected |
48
+
49
+ ---
50
+
51
+ ## 🎖️ Cryptographic Certification & Leaderboard
52
+
53
+ Services that achieve a **CONFORMANT** verdict (Grade A or A+) can request an on-chain signed attestation and permanent verification badge:
54
+
55
+ ```bash
56
+ curl -X POST https://api.automaton-sovereign.workers.dev/v2/conformance/certify \
57
+ -H "Content-Type: application/json" \
58
+ -H "X-PAYMENT: <txHash-or-EIP3009>" \
59
+ -d '{"url":"https://your-service.com/v1/paid"}'
60
+ ```
61
+
62
+ * Certified services appear on the public [x402 Leaderboard](https://api.automaton-sovereign.workers.dev/leaderboard).
63
+ * Fee: 0.05 USDC settled directly on Base L2 to `0x71DEAc098914A009E3720524642A6bE6F65EE528`.
64
+
65
+ ---
66
+
67
+ ## 📄 License
68
+ MIT © Automaton-Sovereign
package/bin/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const engine = require('../index.js');
4
+
5
+ const args = process.argv.slice(2);
6
+ const target = args.find(a => !a.startsWith('--'));
7
+ const jsonMode = args.includes('--json');
8
+ const svgMode = args.includes('--svg');
9
+
10
+ if (!target) {
11
+ console.error('\nUsage: npx @celorodrigues/x402-conformance <endpoint-url> [--json] [--svg]');
12
+ console.error('Example: npx @celorodrigues/x402-conformance https://api.myservice.com/v1/paid --json\n');
13
+ process.exit(2);
14
+ }
15
+
16
+ engine.run(target).then(report => {
17
+ if (jsonMode) {
18
+ console.log(JSON.stringify(report, null, 2));
19
+ } else if (svgMode) {
20
+ console.log(engine.generateBadgeSvg(report));
21
+ } else {
22
+ engine.printCli(report);
23
+ }
24
+ process.exit(report.verdict === 'CONFORMANT' ? 0 : 1);
25
+ }).catch(err => {
26
+ console.error('Fatal conformance error:', err.message);
27
+ process.exit(1);
28
+ });
package/index.js ADDED
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * x402-conformance v2.0.0 -- Autonomous x402 Protocol Conformance Linter & Certification Engine
4
+ * Conforms strictly to official Coinbase x402 specifications (v1 and v2 CAIP-2) & EIP-3009.
5
+ * Author: Automaton-Sovereign (Base: 0x71DEAc098914A009E3720524642A6bE6F65EE528)
6
+ *
7
+ * Battery Checks:
8
+ * S1 [MUST] HTTP 402 challenge returned for unpaid request
9
+ * S2 [MUST] Challenge payload includes valid integer x402Version
10
+ * S3 [MUST] accepts[] present, non-empty, with required spec fields
11
+ * S4 [MUST] Valid network (base / eip155:8453) and checksummed/valid addresses
12
+ * S5 [MUST] EIP-712 domain extra fields (name: 'USD Coin', version: '2')
13
+ * S6 [SHOULD] Resource field is an absolute URI matching target
14
+ * S7 [MUST] Malformed X-PAYMENT header rejected (HTTP 400 or 402, never 200)
15
+ * S8 [MUST] EIP-3009 payload with invalid/tampered signature rejected
16
+ * S9 [MUST] EIP-3009 authorization with expired validBefore rejected
17
+ * S10 [MUST] EIP-3009 authorization with underpaid value rejected
18
+ * S11 [MUST] EIP-3009 authorization with mismatched payTo rejected
19
+ * S12 [INFO] Discovery surface available (/.well-known/x402, openapi, or agent-card)
20
+ */
21
+ 'use strict';
22
+
23
+ const https = require('https');
24
+ const http = require('http');
25
+ const { URL } = require('url');
26
+
27
+ function fetchUrl(targetUrl, options = {}) {
28
+ return new Promise((resolve, reject) => {
29
+ try {
30
+ const u = new URL(targetUrl);
31
+ const lib = u.protocol === 'http:' ? http : https;
32
+ const headers = Object.assign({
33
+ 'User-Agent': 'Automaton-x402-Conformance-Linter/2.0.0',
34
+ 'Accept': 'application/json, text/plain, */*'
35
+ }, options.headers || {});
36
+
37
+ const req = lib.request({
38
+ hostname: u.hostname,
39
+ port: u.port || (u.protocol === 'http:' ? 80 : 443),
40
+ path: u.pathname + u.search,
41
+ method: options.method || 'GET',
42
+ headers,
43
+ timeout: options.timeout || 12000
44
+ }, (res) => {
45
+ let body = '';
46
+ res.on('data', chunk => body += chunk);
47
+ res.on('end', () => resolve({
48
+ status: res.statusCode,
49
+ headers: res.headers,
50
+ body
51
+ }));
52
+ });
53
+
54
+ req.on('error', reject);
55
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
56
+ if (options.body) req.write(options.body);
57
+ req.end();
58
+ } catch (e) {
59
+ reject(e);
60
+ }
61
+ });
62
+ }
63
+
64
+ function parseJsonSafe(str) {
65
+ try { return JSON.parse(str); } catch (e) { return null; }
66
+ }
67
+
68
+ function isAddress(addr) {
69
+ return typeof addr === 'string' && /^0x[0-9a-fA-F]{40}$/.test(addr);
70
+ }
71
+
72
+ function createMockEip3009Payload(overrides = {}) {
73
+ const defaultAuth = {
74
+ from: '0x1111111111111111111111111111111111111111',
75
+ to: '0x71DEAc098914A009E3720524642A6bE6F65EE528',
76
+ value: '1000',
77
+ validAfter: 0,
78
+ validBefore: Math.floor(Date.now() / 1000) + 3600,
79
+ nonce: '0x' + 'aa'.repeat(32)
80
+ };
81
+ const auth = Object.assign(defaultAuth, overrides.authorization || {});
82
+ const payload = {
83
+ x402Version: 1,
84
+ scheme: 'exact',
85
+ network: 'base',
86
+ payload: {
87
+ authorization: auth,
88
+ signature: overrides.signature || ('0x' + 'bb'.repeat(65))
89
+ }
90
+ };
91
+ return Buffer.from(JSON.stringify(payload)).toString('base64');
92
+ }
93
+
94
+ async function run(targetUrl, options = {}) {
95
+ const results = [];
96
+ const add = (id, name, level, status, detail) => results.push({ id, name, level, status, detail });
97
+
98
+ // S1: Unpaid request must return HTTP 402
99
+ let s1Res = null;
100
+ try {
101
+ s1Res = await fetchUrl(targetUrl, {
102
+ headers: { 'X-No-Trial': '1' }
103
+ });
104
+ } catch (e) {
105
+ add('S1', 'HTTP 402 challenge', 'MUST', 'FAIL', `Target unreachable: ${e.message}`);
106
+ return finish(results, targetUrl);
107
+ }
108
+
109
+ const is402 = s1Res.status === 402;
110
+ add('S1', 'HTTP 402 challenge', 'MUST', is402 ? 'PASS' : 'FAIL', `Received HTTP ${s1Res.status}`);
111
+ if (!is402) {
112
+ return finish(results, targetUrl);
113
+ }
114
+
115
+ // Parse challenge body
116
+ const body = parseJsonSafe(s1Res.body);
117
+ const authHeader = s1Res.headers['www-authenticate'] || '';
118
+
119
+ // S2: x402Version present & integer
120
+ const hasVersion = body && (Number.isInteger(body.x402Version) || (body.accepts && body.accepts[0] && Number.isInteger(body.accepts[0].x402Version)));
121
+ const versionNum = body ? (body.x402Version || (body.accepts && body.accepts[0] && body.accepts[0].x402Version)) : null;
122
+ add('S2', 'x402Version specification', 'MUST', hasVersion ? 'PASS' : 'FAIL', hasVersion ? `x402Version=${versionNum}` : 'Missing or non-integer x402Version');
123
+
124
+ // S3: accepts[] valid structure
125
+ const accepts = body && Array.isArray(body.accepts) ? body.accepts : [];
126
+ const primaryAccept = accepts[0] || null;
127
+ const hasAccepts = accepts.length > 0 && primaryAccept && primaryAccept.scheme === 'exact';
128
+ const acceptsDetail = hasAccepts
129
+ ? `accepts[0]: scheme=${primaryAccept.scheme}, asset=${primaryAccept.asset || '(none)'}, amount=${primaryAccept.maxAmountRequired || primaryAccept.amount || '?'}`
130
+ : (accepts.length === 0 ? 'accepts[] is missing or empty' : `scheme=${primaryAccept.scheme || 'unknown'}`);
131
+ add('S3', 'accepts[] exact scheme', 'MUST', hasAccepts ? 'PASS' : 'FAIL', acceptsDetail);
132
+
133
+ // S4: network & address validity
134
+ let netValid = false;
135
+ let netDetail = 'No accepts[]';
136
+ if (primaryAccept) {
137
+ const net = (primaryAccept.network || '').toLowerCase();
138
+ const isBase = net === 'base' || net === 'eip155:8453' || net === 'base-mainnet';
139
+ const payToValid = isAddress(primaryAccept.payTo);
140
+ const assetValid = isAddress(primaryAccept.asset);
141
+ netValid = isBase && payToValid && assetValid;
142
+ netDetail = `network=${net} (Base=${isBase}), payTo=${payToValid ? 'valid' : 'invalid'}, asset=${assetValid ? 'valid' : 'invalid'}`;
143
+ }
144
+ add('S4', 'Base network & valid addresses', 'MUST', netValid ? 'PASS' : 'FAIL', netDetail);
145
+
146
+ // S5: EIP-712 extra metadata
147
+ let extraValid = false;
148
+ let extraDetail = 'extra metadata missing';
149
+ if (primaryAccept && primaryAccept.extra) {
150
+ const name = primaryAccept.extra.name || '';
151
+ const ver = String(primaryAccept.extra.version || '');
152
+ extraValid = (name.includes('USD') || name.includes('Coin')) && ver === '2';
153
+ extraDetail = `extra.name="${name}", extra.version="${ver}"`;
154
+ }
155
+ add('S5', 'EIP-712 domain extra fields', 'MUST', extraValid ? 'PASS' : 'FAIL', extraDetail);
156
+
157
+ // S6: Resource field is absolute URL
158
+ let resValid = false;
159
+ let resDetail = 'No resource field';
160
+ if (primaryAccept && primaryAccept.resource) {
161
+ try {
162
+ const parsedRes = new URL(primaryAccept.resource);
163
+ resValid = parsedRes.protocol === 'http:' || parsedRes.protocol === 'https:';
164
+ resDetail = `resource=${primaryAccept.resource}`;
165
+ } catch (e) {
166
+ resDetail = `Non-absolute resource: ${primaryAccept.resource}`;
167
+ }
168
+ }
169
+ add('S6', 'Absolute resource URI', 'SHOULD', resValid ? 'PASS' : 'FAIL', resDetail);
170
+
171
+ // S7: Malformed X-PAYMENT rejected
172
+ try {
173
+ const malRes = await fetchUrl(targetUrl, {
174
+ headers: { 'X-PAYMENT': '0xnot_a_valid_payload_or_hash', 'X-No-Trial': '1' }
175
+ });
176
+ const malOk = malRes.status === 402 || malRes.status === 400;
177
+ add('S7', 'Malformed payment rejected', 'MUST', malOk ? 'PASS' : 'FAIL', `Malformed header -> HTTP ${malRes.status}`);
178
+ } catch (e) {
179
+ add('S7', 'Malformed payment rejected', 'MUST', 'FAIL', e.message);
180
+ }
181
+
182
+ // S8: EIP-3009 Invalid Signature rejected
183
+ try {
184
+ const badSigPayload = createMockEip3009Payload({
185
+ authorization: { to: (primaryAccept && primaryAccept.payTo) || '0x71DEAc098914A009E3720524642A6bE6F65EE528' },
186
+ signature: '0x' + '00'.repeat(65)
187
+ });
188
+ const s8Res = await fetchUrl(targetUrl, {
189
+ headers: { 'X-PAYMENT': badSigPayload, 'X-No-Trial': '1' }
190
+ });
191
+ const s8Ok = s8Res.status === 402 || s8Res.status === 400;
192
+ add('S8', 'Invalid signature rejected', 'MUST', s8Ok ? 'PASS' : 'FAIL', `Invalid sig -> HTTP ${s8Res.status}`);
193
+ } catch (e) {
194
+ add('S8', 'Invalid signature rejected', 'MUST', 'FAIL', e.message);
195
+ }
196
+
197
+ // S9: Expired validBefore rejected
198
+ try {
199
+ const expiredPayload = createMockEip3009Payload({
200
+ authorization: {
201
+ to: (primaryAccept && primaryAccept.payTo) || '0x71DEAc098914A009E3720524642A6bE6F65EE528',
202
+ validBefore: Math.floor(Date.now() / 1000) - 3600
203
+ }
204
+ });
205
+ const s9Res = await fetchUrl(targetUrl, {
206
+ headers: { 'X-PAYMENT': expiredPayload, 'X-No-Trial': '1' }
207
+ });
208
+ const s9Ok = s9Res.status === 402 || s9Res.status === 400;
209
+ add('S9', 'Expired authorization rejected', 'MUST', s9Ok ? 'PASS' : 'FAIL', `Expired validBefore -> HTTP ${s9Res.status}`);
210
+ } catch (e) {
211
+ add('S9', 'Expired authorization rejected', 'MUST', 'FAIL', e.message);
212
+ }
213
+
214
+ // S10: Underpaid value rejected
215
+ try {
216
+ const underpaidPayload = createMockEip3009Payload({
217
+ authorization: {
218
+ to: (primaryAccept && primaryAccept.payTo) || '0x71DEAc098914A009E3720524642A6bE6F65EE528',
219
+ value: '1'
220
+ }
221
+ });
222
+ const s10Res = await fetchUrl(targetUrl, {
223
+ headers: { 'X-PAYMENT': underpaidPayload, 'X-No-Trial': '1' }
224
+ });
225
+ const s10Ok = s10Res.status === 402 || s10Res.status === 400;
226
+ add('S10', 'Underpaid authorization rejected', 'MUST', s10Ok ? 'PASS' : 'FAIL', `Underpaid value=1 -> HTTP ${s10Res.status}`);
227
+ } catch (e) {
228
+ add('S10', 'Underpaid authorization rejected', 'MUST', 'FAIL', e.message);
229
+ }
230
+
231
+ // S11: Mismatched payTo recipient rejected
232
+ try {
233
+ const wrongToPayload = createMockEip3009Payload({
234
+ authorization: {
235
+ to: '0x0000000000000000000000000000000000000001'
236
+ }
237
+ });
238
+ const s11Res = await fetchUrl(targetUrl, {
239
+ headers: { 'X-PAYMENT': wrongToPayload, 'X-No-Trial': '1' }
240
+ });
241
+ const s11Ok = s11Res.status === 402 || s11Res.status === 400;
242
+ add('S11', 'Wrong payTo rejected', 'MUST', s11Ok ? 'PASS' : 'FAIL', `Wrong recipient to=0x00...01 -> HTTP ${s11Res.status}`);
243
+ } catch (e) {
244
+ add('S11', 'Wrong payTo rejected', 'MUST', 'FAIL', e.message);
245
+ }
246
+
247
+ // S12: Discovery surface (INFO)
248
+ try {
249
+ const targetOrigin = new URL(targetUrl).origin;
250
+ const discRes = await fetchUrl(targetOrigin + '/.well-known/x402');
251
+ const discOk = discRes.status === 200;
252
+ add('S12', 'Discovery surface available', 'INFO', discOk ? 'PASS' : 'SKIP', `GET /.well-known/x402 -> HTTP ${discRes.status}`);
253
+ } catch (e) {
254
+ add('S12', 'Discovery surface available', 'INFO', 'SKIP', 'Discovery not detected');
255
+ }
256
+
257
+ return finish(results, targetUrl);
258
+ }
259
+
260
+ function finish(results, targetUrl) {
261
+ const mustChecks = results.filter(r => r.level === 'MUST');
262
+ const mustPassed = mustChecks.filter(r => r.status === 'PASS').length;
263
+ const mustTotal = mustChecks.length;
264
+
265
+ const allPassed = results.filter(r => r.status === 'PASS').length;
266
+ const allScored = results.filter(r => r.status !== 'SKIP').length;
267
+ const scorePercent = allScored > 0 ? Math.round((allPassed / allScored) * 100) : 0;
268
+
269
+ const isConformant = mustPassed === mustTotal && mustTotal >= 6;
270
+ const grade = scorePercent >= 95 ? 'A+' : (scorePercent >= 85 ? 'A' : (scorePercent >= 70 ? 'B' : 'C'));
271
+
272
+ return {
273
+ suite: '@celorodrigues/x402-conformance',
274
+ version: '2.0.0',
275
+ target: targetUrl,
276
+ timestamp: new Date().toISOString(),
277
+ verdict: isConformant ? 'CONFORMANT' : 'NON_CONFORMANT',
278
+ grade,
279
+ score: scorePercent,
280
+ stats: {
281
+ mustPassed,
282
+ mustTotal,
283
+ allPassed,
284
+ allScored,
285
+ totalChecks: results.length
286
+ },
287
+ results
288
+ };
289
+ }
290
+
291
+ function generateBadgeSvg(report) {
292
+ const isConformant = report.verdict === 'CONFORMANT';
293
+ const color = isConformant ? '#10B981' : '#EF4444';
294
+ const label = 'x402-conformance';
295
+ const status = isConformant ? `Verified ${report.grade}` : 'Non-Compliant';
296
+
297
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="180" height="24" viewBox="0 0 180 24" role="img" aria-label="${label}: ${status}">
298
+ <linearGradient id="g" x2="0" y2="100%">
299
+ <stop offset="0" stop-color="#1e293b" stop-opacity=".9"/>
300
+ <stop offset="100%" stop-color="#0f172a" stop-opacity=".95"/>
301
+ </linearGradient>
302
+ <clipPath id="r">
303
+ <rect width="180" height="24" rx="4" fill="#fff"/>
304
+ </clipPath>
305
+ <g clip-path="url(#r)">
306
+ <rect width="105" height="24" fill="#0f172a"/>
307
+ <rect x="105" width="75" height="24" fill="${color}"/>
308
+ <rect width="180" height="24" fill="url(#g)" opacity="0.1"/>
309
+ </g>
310
+ <g fill="#fff" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif" text-rendering="geometricPrecision" font-size="11">
311
+ <text x="52" y="16" fill="#94a3b8" font-weight="600">x402-standard</text>
312
+ <text x="142" y="16" fill="#fff" font-weight="700">${status}</text>
313
+ </g>
314
+ </svg>`;
315
+ }
316
+
317
+ function printCli(report) {
318
+ console.log(`\n🛡️ x402 Conformance Linter v${report.version}`);
319
+ console.log(`Target: ${report.target}`);
320
+ console.log(`Timestamp: ${report.timestamp}`);
321
+ console.log('─'.repeat(74));
322
+ for (const r of report.results) {
323
+ const icon = r.status === 'PASS' ? '\x1b[32m✔ PASS\x1b[0m' : (r.status === 'FAIL' ? '\x1b[31m✘ FAIL\x1b[0m' : '\x1b[90m○ SKIP\x1b[0m');
324
+ const lvl = `[${r.level}]`.padEnd(8);
325
+ console.log(`${icon} ${lvl} ${r.id.padEnd(4)} ${r.name.padEnd(30)} ${r.detail}`);
326
+ }
327
+ console.log('─'.repeat(74));
328
+ const verdictColor = report.verdict === 'CONFORMANT' ? '\x1b[32m' : '\x1b[31m';
329
+ console.log(`VERDICT: ${verdictColor}${report.verdict}\x1b[0m | Grade: \x1b[1m${report.grade}\x1b[0m (${report.score}%) | MUST: ${report.stats.mustPassed}/${report.stats.mustTotal}\n`);
330
+ }
331
+
332
+ if (require.main === module) {
333
+ const args = process.argv.slice(2);
334
+ const target = args.find(a => !a.startsWith('--'));
335
+ const jsonMode = args.includes('--json');
336
+ const svgMode = args.includes('--svg');
337
+
338
+ if (!target) {
339
+ console.error('Usage: node x402-conformance-v2.js <url> [--json] [--svg]');
340
+ process.exit(2);
341
+ }
342
+
343
+ run(target).then(report => {
344
+ if (jsonMode) {
345
+ console.log(JSON.stringify(report, null, 2));
346
+ } else if (svgMode) {
347
+ console.log(generateBadgeSvg(report));
348
+ } else {
349
+ printCli(report);
350
+ }
351
+ process.exit(report.verdict === 'CONFORMANT' ? 0 : 1);
352
+ }).catch(err => {
353
+ console.error('Fatal error:', err.message);
354
+ process.exit(1);
355
+ });
356
+ }
357
+
358
+ module.exports = {
359
+ run,
360
+ generateBadgeSvg,
361
+ printCli
362
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@celorodrigues/x402-conformance",
3
+ "version": "2.0.0",
4
+ "description": "Zero-dependency CLI & testing suite for official x402 AI agent micropayment protocol & EIP-3009 compliance",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "x402-conformance": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "bin/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "x402",
17
+ "eip-3009",
18
+ "coinbase",
19
+ "base",
20
+ "ai-agents",
21
+ "micropayments",
22
+ "usdc",
23
+ "linter",
24
+ "conformance"
25
+ ],
26
+ "author": "Automaton-Sovereign (0x71DEAc098914A009E3720524642A6bE6F65EE528)",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/baianomarceloeduardo-jpg/automaton-x402.git"
34
+ },
35
+ "homepage": "https://api.automaton-sovereign.workers.dev/leaderboard",
36
+ "bugs": {
37
+ "url": "https://github.com/baianomarceloeduardo-jpg/automaton-x402/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }