x402-surface-check 0.2.9 → 0.2.11

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
@@ -20,12 +20,14 @@ npx --yes x402-surface-check --endpoint --method POST https://x402.rpc.ankr.com/
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
26
27
  - Browser CORS allowance for `X-PAYMENT`
27
28
  - Over-broad public method surfaces
28
29
  - Auth, validation, and free/trial responses that appear before a payment challenge, without piling on missing-field findings when no challenge was actually returned
30
+ - Operational health/status endpoints, without treating expected free health checks as paid-route failures
29
31
  - Object-valued document metadata such as facilitator objects, without `[object Object]` report artifacts
30
32
 
31
33
  ## Public Proof
@@ -35,6 +37,8 @@ Recent public no-payment checks have found and verified real launch fixes:
35
37
  - TensorFeed: parameter-required premium routes moved behind canonical x402 V2 challenges, then verified clean. https://github.com/solana-foundation/pay-skills/pull/68#issuecomment-4455360068
36
38
  - x402jp: weather routes that returned 500 now return structured Base x402 challenges. https://github.com/solana-foundation/pay-skills/pull/58#issuecomment-4455401355
37
39
  - Spraay: resource echo and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/60#issuecomment-4455519760
40
+ - UZPROOF: schemes-style Solana x402 challenge and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/28#issuecomment-4455613892
41
+ - HYRE Agent: OpenAPI-declared prices found 10x below live 402 challenge prices. https://github.com/solana-foundation/pay-skills/pull/19#issuecomment-4455641258
38
42
  - Agent Trust Bench: live discovery URL and browser-compatibility notes for adversarial agent-payment resources. https://github.com/solana-foundation/pay-skills/pull/23#issuecomment-4455484414
39
43
 
40
44
  Field notes and browser version: https://tateprograms.com/x402-surface-check.html
@@ -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
  }
@@ -437,6 +452,14 @@ function challengePrice(accept, result) {
437
452
  : moneyFromAtomic(amount, acceptDecimals(accept))
438
453
  }
439
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
+
440
463
  function hasPaymentChallenge(result) {
441
464
  const challenge = result.body.json
442
465
  return challengeAccepts(result).length > 0 || Boolean(challenge?.resource || challenge?.payment || result.headers?.['www-authenticate'])
@@ -457,6 +480,8 @@ function challengeSummary(result) {
457
480
  network: firstAccept.network ?? '',
458
481
  amount,
459
482
  price: hasChallenge ? challengePrice(firstAccept, result) : '',
483
+ priceUsd: hasChallenge ? challengePriceUsd(firstAccept, result) : null,
484
+ expectedPriceUsd: typeof result.expectedPriceUsd === 'number' ? result.expectedPriceUsd : null,
460
485
  payTo: firstAccept.payTo ?? '',
461
486
  asset: acceptAssetValue(firstAccept),
462
487
  timeout: firstAccept.maxTimeoutSeconds ?? '',
@@ -476,11 +501,21 @@ function looksLikePlaceholderPayTo(payTo) {
476
501
  return false
477
502
  }
478
503
 
504
+ function entryKey(entry) {
505
+ return `${entry.method ?? 'POST'} ${entry.url}`
506
+ }
507
+
508
+ function looksLikeOperationalHealthEndpoint(result) {
509
+ const value = `${result.name ?? ''} ${new URL(result.url).pathname}`.toLowerCase()
510
+ return /(^|[/_\s-])(health|healthz|ready|readiness|live|liveness|status)([/_\s-]|$)/.test(value)
511
+ }
512
+
479
513
  function findingList(documentResult, challengeResults, preflightResults, entries) {
480
514
  const document = documentResult.body.json ?? {}
481
515
  const findings = []
482
516
  const networks = valueList(document.networks)
483
517
  const challengeNetworks = new Set()
518
+ const challengesByEntry = new Map(challengeResults.map(result => [entryKey(result), result]))
484
519
 
485
520
  if (documentResult.status < 200 || documentResult.status >= 300) {
486
521
  findings.push(`P1 - Document returned HTTP ${documentResult.status}; expected a successful JSON response.`)
@@ -501,7 +536,9 @@ function findingList(documentResult, challengeResults, preflightResults, entries
501
536
 
502
537
  if (result.status !== 402) {
503
538
  if (result.status >= 200 && result.status < 300) {
504
- findings.push(`P3 - ${result.name} returned ${result.status} without a payment challenge for a no-payment ${result.method ?? 'POST'} probe; document this as free/trial access or move the 402 challenge before content.`)
539
+ if (!looksLikeOperationalHealthEndpoint(result)) {
540
+ findings.push(`P3 - ${result.name} returned ${result.status} without a payment challenge for a no-payment ${result.method ?? 'POST'} probe; document this as free/trial access or move the 402 challenge before content.`)
541
+ }
505
542
  }
506
543
  else if (result.status === 400 || result.status === 422) {
507
544
  findings.push(`P1 - ${result.name} returned validation HTTP ${result.status} before a payment challenge for a no-payment ${result.method ?? 'POST'} probe.`)
@@ -524,6 +561,12 @@ function findingList(documentResult, challengeResults, preflightResults, entries
524
561
  if (!summary.amount || !summary.payTo || !summary.asset) {
525
562
  findings.push(`P1 - ${result.name} challenge is missing amount/maxAmountRequired, payTo, or asset metadata.`)
526
563
  }
564
+ if (summary.expectedPriceUsd !== null && summary.priceUsd !== null) {
565
+ const delta = Math.abs(summary.expectedPriceUsd - summary.priceUsd)
566
+ if (delta > 0.000001) {
567
+ findings.push(`P1 - ${result.name} documented price ${moneyFromDecimal(summary.expectedPriceUsd)} does not match live 402 challenge price ${moneyFromDecimal(summary.priceUsd)}.`)
568
+ }
569
+ }
527
570
  for (const accept of challengeAccepts(result)) {
528
571
  if (looksLikePlaceholderPayTo(accept.payTo)) {
529
572
  findings.push(`P1 - ${result.name} challenge advertises placeholder-looking payTo ${accept.payTo}; production listings should not ask agents to pay placeholder recipients.`)
@@ -538,9 +581,14 @@ function findingList(documentResult, challengeResults, preflightResults, entries
538
581
  }
539
582
 
540
583
  for (const result of preflightResults) {
584
+ const challengeResult = challengesByEntry.get(entryKey(result))
585
+ if (!challengeResult || !hasPaymentChallenge(challengeResult)) continue
541
586
  const allowed = result.headers['access-control-allow-headers'] ?? ''
542
587
  if (allowed !== '*' && !/x-payment/i.test(allowed)) {
543
- findings.push(`P1 - ${result.name} CORS preflight does not allow X-PAYMENT; observed allow headers: ${allowed || 'none'}.`)
588
+ const observed = result.status >= 400
589
+ ? `HTTP ${result.status}; allow headers: ${allowed || 'none'}`
590
+ : `allow headers: ${allowed || 'none'}`
591
+ findings.push(`P1 - ${result.name} CORS preflight does not allow X-PAYMENT; observed ${observed}.`)
544
592
  }
545
593
  const allowedMethods = result.headers['access-control-allow-methods'] ?? ''
546
594
  if (/delete|put|patch/i.test(allowedMethods)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x402-surface-check",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "No-payment x402 public-surface checker for manifests, OpenAPI specs, and HTTP 402 challenges.",
5
5
  "type": "module",
6
6
  "bin": {