x402-surface-check 0.2.9 → 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 +1 -0
- package/bin/x402-surface-check.mjs +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ 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
|
|
@@ -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 ?? '',
|
|
@@ -524,6 +549,12 @@ function findingList(documentResult, challengeResults, preflightResults, entries
|
|
|
524
549
|
if (!summary.amount || !summary.payTo || !summary.asset) {
|
|
525
550
|
findings.push(`P1 - ${result.name} challenge is missing amount/maxAmountRequired, payTo, or asset metadata.`)
|
|
526
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
|
+
}
|
|
527
558
|
for (const accept of challengeAccepts(result)) {
|
|
528
559
|
if (looksLikePlaceholderPayTo(accept.payTo)) {
|
|
529
560
|
findings.push(`P1 - ${result.name} challenge advertises placeholder-looking payTo ${accept.payTo}; production listings should not ask agents to pay placeholder recipients.`)
|