x402-surface-check 0.2.8 → 0.2.10

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
@@ -16,10 +16,11 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
16
16
 
17
17
  - Manifest endpoint discovery from `items[]`, `endpoints[]`, `x402Endpoints`, category arrays, resource strings, and OpenAPI paths
18
18
  - No-payment HTTP 402 challenge shape
19
- - x402 v1 and v2 price fields
19
+ - x402 v1 and v2 price fields, including `accepts[]` and `schemes[]` challenge arrays
20
20
  - MPP `WWW-Authenticate: Payment` and x402 V2 `WWW-Authenticate: X402 requirements=...` challenges
21
21
  - Atomic-unit `amount` / `maxAmountRequired` fields, plus legacy decimal `amount` + `token` x402 v1 challenges
22
22
  - `asset` or token metadata, `network`, and `payTo`
23
+ - OpenAPI-declared `x-payment-info.price.amount` drift versus the live 402 challenge price
23
24
  - Placeholder recipients such as zero addresses and Solana system-program values
24
25
  - Testnet or staging rails such as Base Sepolia and Solana devnet
25
26
  - HTTPS resource URLs and stable resource metadata
@@ -98,6 +98,11 @@ function moneyFromDecimal(amount) {
98
98
  })}`
99
99
  }
100
100
 
101
+ function numberFromDecimal(amount) {
102
+ const numeric = Number(amount)
103
+ return Number.isFinite(numeric) ? numeric : null
104
+ }
105
+
101
106
  function uniqueEntries(entries, limit) {
102
107
  const seen = new Set()
103
108
  return entries
@@ -127,6 +132,15 @@ function endpointUrl(rawPath, baseUrl, sourceUrl) {
127
132
  return new URL(value, base).toString()
128
133
  }
129
134
 
135
+ function operationExpectedPrice(operation) {
136
+ const price = operation?.['x-payment-info']?.price
137
+ ?? operation?.['x-payment']?.price
138
+ ?? operation?.payment?.price
139
+ const amount = price?.amount ?? price?.amountUsd ?? price?.usd
140
+ const numeric = numberFromDecimal(amount)
141
+ return numeric === null ? null : numeric
142
+ }
143
+
130
144
  function endpointEntries(document, sourceUrl, limit) {
131
145
  const entries = []
132
146
  const baseUrl = documentBaseUrl(document, sourceUrl)
@@ -189,6 +203,7 @@ function endpointEntries(document, sourceUrl, limit) {
189
203
  name: operation.operationId ?? `${method.toUpperCase()} ${path}`,
190
204
  url,
191
205
  method: method.toUpperCase(),
206
+ expectedPriceUsd: operationExpectedPrice(operation),
192
207
  })
193
208
  }
194
209
  }
@@ -332,8 +347,9 @@ async function probeEndpoint(entry) {
332
347
  const authenticateChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
333
348
  ?? parseX402Authenticate(response.headers.get('www-authenticate'))
334
349
 
335
- if (!body.json?.accepts?.length) {
336
- if (headerChallenge) {
350
+ const bodyHasChallenge = Array.isArray(body.json?.accepts) || Array.isArray(body.json?.schemes)
351
+ if (!bodyHasChallenge) {
352
+ if (headerChallenge && typeof headerChallenge === 'object') {
337
353
  body.json = headerChallenge
338
354
  }
339
355
  else if (authenticateChallenge) {
@@ -400,7 +416,9 @@ function capabilityList(value) {
400
416
  }
401
417
 
402
418
  function challengeAccepts(result) {
403
- return Array.isArray(result.body.json?.accepts) ? result.body.json.accepts : []
419
+ if (Array.isArray(result.body.json?.accepts)) return result.body.json.accepts
420
+ if (Array.isArray(result.body.json?.schemes)) return result.body.json.schemes
421
+ return []
404
422
  }
405
423
 
406
424
  function acceptAmountValue(accept) {
@@ -418,10 +436,11 @@ function acceptDecimals(accept) {
418
436
  }
419
437
 
420
438
  function usesDecimalAmount(accept, result) {
421
- if (accept.maxAmountRequired !== undefined || accept.maxAmount !== undefined) return false
422
- if (accept.amount === undefined || accept.amount === null || accept.amount === '') return false
423
- const amount = String(accept.amount)
439
+ const rawAmount = acceptAmountValue(accept)
440
+ if (rawAmount === undefined || rawAmount === null || rawAmount === '') return false
441
+ const amount = String(rawAmount)
424
442
  if (amount.includes('.')) return true
443
+ if (accept.maxAmountRequired !== undefined || accept.maxAmount !== undefined) return false
425
444
  if (!accept.asset && (accept.token || result.headers?.['x-payment-token'])) return true
426
445
  return result.headers?.['x-payment-amount'] === amount
427
446
  }
@@ -433,6 +452,14 @@ function challengePrice(accept, result) {
433
452
  : moneyFromAtomic(amount, acceptDecimals(accept))
434
453
  }
435
454
 
455
+ function challengePriceUsd(accept, result) {
456
+ const amount = acceptAmountValue(accept)
457
+ if (usesDecimalAmount(accept, result)) return numberFromDecimal(amount)
458
+ const numeric = Number(amount)
459
+ if (!Number.isFinite(numeric)) return null
460
+ return numeric / (10 ** acceptDecimals(accept))
461
+ }
462
+
436
463
  function hasPaymentChallenge(result) {
437
464
  const challenge = result.body.json
438
465
  return challengeAccepts(result).length > 0 || Boolean(challenge?.resource || challenge?.payment || result.headers?.['www-authenticate'])
@@ -440,7 +467,7 @@ function hasPaymentChallenge(result) {
440
467
 
441
468
  function challengeSummary(result) {
442
469
  const challenge = result.body.json
443
- const firstAccept = challenge?.accepts?.[0] ?? {}
470
+ const firstAccept = challengeAccepts(result)[0] ?? {}
444
471
  const hasChallenge = hasPaymentChallenge(result)
445
472
  const amount = acceptAmountValue(firstAccept)
446
473
  const resourceUrl = challenge?.resource?.url ?? firstAccept.resource ?? ''
@@ -453,6 +480,8 @@ function challengeSummary(result) {
453
480
  network: firstAccept.network ?? '',
454
481
  amount,
455
482
  price: hasChallenge ? challengePrice(firstAccept, result) : '',
483
+ priceUsd: hasChallenge ? challengePriceUsd(firstAccept, result) : null,
484
+ expectedPriceUsd: typeof result.expectedPriceUsd === 'number' ? result.expectedPriceUsd : null,
456
485
  payTo: firstAccept.payTo ?? '',
457
486
  asset: acceptAssetValue(firstAccept),
458
487
  timeout: firstAccept.maxTimeoutSeconds ?? '',
@@ -520,6 +549,12 @@ function findingList(documentResult, challengeResults, preflightResults, entries
520
549
  if (!summary.amount || !summary.payTo || !summary.asset) {
521
550
  findings.push(`P1 - ${result.name} challenge is missing amount/maxAmountRequired, payTo, or asset metadata.`)
522
551
  }
552
+ if (summary.expectedPriceUsd !== null && summary.priceUsd !== null) {
553
+ const delta = Math.abs(summary.expectedPriceUsd - summary.priceUsd)
554
+ if (delta > 0.000001) {
555
+ findings.push(`P1 - ${result.name} documented price ${moneyFromDecimal(summary.expectedPriceUsd)} does not match live 402 challenge price ${moneyFromDecimal(summary.priceUsd)}.`)
556
+ }
557
+ }
523
558
  for (const accept of challengeAccepts(result)) {
524
559
  if (looksLikePlaceholderPayTo(accept.payTo)) {
525
560
  findings.push(`P1 - ${result.name} challenge advertises placeholder-looking payTo ${accept.payTo}; production listings should not ask agents to pay placeholder recipients.`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {