x402-surface-check 0.2.7 → 0.2.9
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 +15 -2
- package/bin/x402-surface-check.mjs +52 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,15 +16,28 @@ 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
|
-
- `amount` / `maxAmountRequired
|
|
21
|
+
- Atomic-unit `amount` / `maxAmountRequired` fields, plus legacy decimal `amount` + `token` x402 v1 challenges
|
|
22
|
+
- `asset` or token metadata, `network`, and `payTo`
|
|
22
23
|
- Placeholder recipients such as zero addresses and Solana system-program values
|
|
23
24
|
- Testnet or staging rails such as Base Sepolia and Solana devnet
|
|
24
25
|
- HTTPS resource URLs and stable resource metadata
|
|
25
26
|
- Browser CORS allowance for `X-PAYMENT`
|
|
26
27
|
- Over-broad public method surfaces
|
|
27
28
|
- Auth, validation, and free/trial responses that appear before a payment challenge, without piling on missing-field findings when no challenge was actually returned
|
|
29
|
+
- Object-valued document metadata such as facilitator objects, without `[object Object]` report artifacts
|
|
30
|
+
|
|
31
|
+
## Public Proof
|
|
32
|
+
|
|
33
|
+
Recent public no-payment checks have found and verified real launch fixes:
|
|
34
|
+
|
|
35
|
+
- 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
|
+
- x402jp: weather routes that returned 500 now return structured Base x402 challenges. https://github.com/solana-foundation/pay-skills/pull/58#issuecomment-4455401355
|
|
37
|
+
- Spraay: resource echo and browser payment-header behavior verified clean. https://github.com/solana-foundation/pay-skills/pull/60#issuecomment-4455519760
|
|
38
|
+
- 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
|
+
|
|
40
|
+
Field notes and browser version: https://tateprograms.com/x402-surface-check.html
|
|
28
41
|
|
|
29
42
|
## Options
|
|
30
43
|
|
|
@@ -88,6 +88,16 @@ function moneyFromAtomic(amount, decimals = 6) {
|
|
|
88
88
|
})}`
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function moneyFromDecimal(amount) {
|
|
92
|
+
if (amount === '' || amount === null || amount === undefined) return ''
|
|
93
|
+
const numeric = Number(amount)
|
|
94
|
+
if (!Number.isFinite(numeric)) return String(amount ?? '')
|
|
95
|
+
return `$${numeric.toLocaleString(undefined, {
|
|
96
|
+
maximumFractionDigits: 6,
|
|
97
|
+
minimumFractionDigits: numeric < 0.01 ? 3 : 2,
|
|
98
|
+
})}`
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
function uniqueEntries(entries, limit) {
|
|
92
102
|
const seen = new Set()
|
|
93
103
|
return entries
|
|
@@ -264,6 +274,7 @@ function parsePaymentAuthenticate(value) {
|
|
|
264
274
|
maxTimeoutSeconds: '',
|
|
265
275
|
extra: {
|
|
266
276
|
description: request.description ?? '',
|
|
277
|
+
decimals: request.methodDetails?.decimals ?? '',
|
|
267
278
|
expires: params.expires ?? '',
|
|
268
279
|
id: params.id ?? '',
|
|
269
280
|
intent: params.intent ?? '',
|
|
@@ -321,8 +332,9 @@ async function probeEndpoint(entry) {
|
|
|
321
332
|
const authenticateChallenge = parsePaymentAuthenticate(response.headers.get('www-authenticate'))
|
|
322
333
|
?? parseX402Authenticate(response.headers.get('www-authenticate'))
|
|
323
334
|
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
const bodyHasChallenge = Array.isArray(body.json?.accepts) || Array.isArray(body.json?.schemes)
|
|
336
|
+
if (!bodyHasChallenge) {
|
|
337
|
+
if (headerChallenge && typeof headerChallenge === 'object') {
|
|
326
338
|
body.json = headerChallenge
|
|
327
339
|
}
|
|
328
340
|
else if (authenticateChallenge) {
|
|
@@ -389,7 +401,40 @@ function capabilityList(value) {
|
|
|
389
401
|
}
|
|
390
402
|
|
|
391
403
|
function challengeAccepts(result) {
|
|
392
|
-
|
|
404
|
+
if (Array.isArray(result.body.json?.accepts)) return result.body.json.accepts
|
|
405
|
+
if (Array.isArray(result.body.json?.schemes)) return result.body.json.schemes
|
|
406
|
+
return []
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function acceptAmountValue(accept) {
|
|
410
|
+
return accept.maxAmountRequired ?? accept.maxAmount ?? accept.amount ?? ''
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function acceptAssetValue(accept) {
|
|
414
|
+
return accept.asset ?? accept.token ?? accept.currency ?? ''
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function acceptDecimals(accept) {
|
|
418
|
+
const value = accept.decimals ?? accept.extra?.decimals ?? accept.methodDetails?.decimals
|
|
419
|
+
const numeric = Number(value)
|
|
420
|
+
return Number.isFinite(numeric) ? numeric : 6
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function usesDecimalAmount(accept, result) {
|
|
424
|
+
const rawAmount = acceptAmountValue(accept)
|
|
425
|
+
if (rawAmount === undefined || rawAmount === null || rawAmount === '') return false
|
|
426
|
+
const amount = String(rawAmount)
|
|
427
|
+
if (amount.includes('.')) return true
|
|
428
|
+
if (accept.maxAmountRequired !== undefined || accept.maxAmount !== undefined) return false
|
|
429
|
+
if (!accept.asset && (accept.token || result.headers?.['x-payment-token'])) return true
|
|
430
|
+
return result.headers?.['x-payment-amount'] === amount
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function challengePrice(accept, result) {
|
|
434
|
+
const amount = acceptAmountValue(accept)
|
|
435
|
+
return usesDecimalAmount(accept, result)
|
|
436
|
+
? moneyFromDecimal(amount)
|
|
437
|
+
: moneyFromAtomic(amount, acceptDecimals(accept))
|
|
393
438
|
}
|
|
394
439
|
|
|
395
440
|
function hasPaymentChallenge(result) {
|
|
@@ -399,9 +444,9 @@ function hasPaymentChallenge(result) {
|
|
|
399
444
|
|
|
400
445
|
function challengeSummary(result) {
|
|
401
446
|
const challenge = result.body.json
|
|
402
|
-
const firstAccept =
|
|
447
|
+
const firstAccept = challengeAccepts(result)[0] ?? {}
|
|
403
448
|
const hasChallenge = hasPaymentChallenge(result)
|
|
404
|
-
const amount = firstAccept
|
|
449
|
+
const amount = acceptAmountValue(firstAccept)
|
|
405
450
|
const resourceUrl = challenge?.resource?.url ?? firstAccept.resource ?? ''
|
|
406
451
|
const extraResource = firstAccept.extra?.resource ?? firstAccept.resource ?? ''
|
|
407
452
|
|
|
@@ -411,9 +456,9 @@ function challengeSummary(result) {
|
|
|
411
456
|
resourceUrl,
|
|
412
457
|
network: firstAccept.network ?? '',
|
|
413
458
|
amount,
|
|
414
|
-
price:
|
|
459
|
+
price: hasChallenge ? challengePrice(firstAccept, result) : '',
|
|
415
460
|
payTo: firstAccept.payTo ?? '',
|
|
416
|
-
asset: firstAccept
|
|
461
|
+
asset: acceptAssetValue(firstAccept),
|
|
417
462
|
timeout: firstAccept.maxTimeoutSeconds ?? '',
|
|
418
463
|
extraResource,
|
|
419
464
|
}
|