agent-receipt-verify 0.0.0-stage → 0.2.1

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.
Files changed (37) hide show
  1. package/CONFORMANCE.md +110 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +39 -0
  4. package/README.md +256 -2
  5. package/bin/cli.mjs +89 -0
  6. package/examples/receipt-live-base-sepolia-2.json +21 -0
  7. package/examples/receipt-live-base-sepolia-3.json +21 -0
  8. package/examples/receipt-live-base-sepolia.json +21 -0
  9. package/package.json +59 -4
  10. package/profiles/profile.schema.json +115 -0
  11. package/profiles/x402-deployed-ladder-v1.json +178 -0
  12. package/src/eip3009-autorisatie.mjs +195 -0
  13. package/src/inam-countersigned.mjs +162 -0
  14. package/src/mcp-server.mjs +262 -0
  15. package/src/profiel.mjs +145 -0
  16. package/src/x402-conformance-check.mjs +194 -0
  17. package/src/x402-conformance-treden.mjs +246 -0
  18. package/src/x402-discovery-census.mjs +247 -0
  19. package/src/x402-index-liveness.mjs +201 -0
  20. package/src/x402-link-onchain.mjs +280 -0
  21. package/src/x402-metingen-bundel.mjs +96 -0
  22. package/src/x402-offer-receipt.mjs +84 -0
  23. package/src/x402-poortwachters.mjs +241 -0
  24. package/tools/bundel-bouw.mjs +126 -0
  25. package/vectors/inam/BRON-README.md +38 -0
  26. package/vectors/inam/canonical-vectors.json +122 -0
  27. package/vectors/inam/vectors.json +96 -0
  28. package/vectors/kanon/amount-insufficient-001.json +52 -0
  29. package/vectors/kanon/cross-chain-replay-001.json +54 -0
  30. package/vectors/kanon/cross-contract-replay-001.json +54 -0
  31. package/vectors/kanon/domain-name-mismatch-001.json +53 -0
  32. package/vectors/kanon/expired-001.json +51 -0
  33. package/vectors/kanon/network-mismatch-001.json +52 -0
  34. package/vectors/kanon/nonce-replay-001.json +56 -0
  35. package/vectors/kanon/not-yet-valid-001.json +51 -0
  36. package/vectors/kanon/sig-malleable-high-s-001.json +54 -0
  37. package/vectors/kanon/valid-baseline-001.json +52 -0
package/bin/cli.mjs ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ // Een commando in plaats van een document.
3
+ //
4
+ // WAAROM DIT BESTAAT. Niemand neemt een meetlat over omdat die in een bestand staat.
5
+ // Mensen nemen hem over als het makkelijkste wat ze kunnen doen een regel is die hun
6
+ // eigen eindpunt nakijkt. Dan is de ladder de norm geworden zonder dat er ooit over
7
+ // gestemd is.
8
+ //
9
+ // BEWUST GEEN NPM-PUBLICATIE NODIG. npx kan een pakket rechtstreeks uit een git-repo
10
+ // draaien, dus dit werkt zonder dat er iets op npm staat:
11
+ //
12
+ // npx github:holistis/agent-receipt-verify https://een-eindpunt.example/pad
13
+ //
14
+ // Dat is geen trucje maar een echte eigenschap van npx, en het haalt een hele
15
+ // publicatie-stap plus een naamclaim weg. Het betekent ook dat niemand een
16
+ // tussenversie van ons kan uitgeven: wat je draait is wat er in de repo staat.
17
+ //
18
+ // ethers is OPTIONEEL. Zonder ethers kan trede 2 van een eip712-aanbod niet
19
+ // gecontroleerd worden, en dan zegt de uitslag ONGEMETEN in plaats van stil een
20
+ // zwakkere controle te doen. Een jws-aanbod werkt wel zonder ethers, want dat gaat
21
+ // op ingebouwde crypto.
22
+
23
+ const args = process.argv.slice(2)
24
+ const eerste = args[0]
25
+
26
+ function gebruik() {
27
+ console.log('agent-receipt-verify, een onafhankelijke controle op bonnetjes van agent-betalingen')
28
+ console.log('')
29
+ console.log(' npx github:holistis/agent-receipt-verify <url> [url ...]')
30
+ console.log(' meet een x402-eindpunt tegen de ladder in CONFORMANCE.md (trede 0 tot 2,')
31
+ console.log(' zonder te betalen). Trede 3 tot 5 vereisen een echte afrekening en komen')
32
+ console.log(' terug als ONGEMETEN, nooit als afgekeurd.')
33
+ console.log('')
34
+ console.log(' npx github:holistis/agent-receipt-verify census [uit.json]')
35
+ console.log(' telt een discovery-index per host in plaats van per resource.')
36
+ console.log('')
37
+ console.log(' npx github:holistis/agent-receipt-verify link --receipt=<bestand>')
38
+ console.log(' zoekt bij een bonnetje van versie 2 de bijbehorende afrekening op de keten.')
39
+ console.log('')
40
+ console.log(' npx github:holistis/agent-receipt-verify mcp')
41
+ console.log(' draait als MCP-server over stdio, zodat een agent deze controle AANROEPT')
42
+ console.log(' in plaats van hem aanbevolen te krijgen. Twee gereedschappen: controleer een')
43
+ console.log(' eindpunt voor je betaalt, en link een bonnetje aan een echte afrekening.')
44
+ console.log('')
45
+ console.log(' npx github:holistis/agent-receipt-verify inam <vectors.json>')
46
+ console.log(' controleert countersigned bonnetjes tegen gepubliceerde vectoren.')
47
+ console.log('')
48
+ console.log('Wat dit NIET doet: het oordeelt over de VORM van een bonnetje, niet over de')
49
+ console.log('eerlijkheid van de partij die het afgaf. Zie CONFORMANCE.md voor wat met opzet')
50
+ console.log('niet gemeten wordt.')
51
+ console.log('')
52
+ console.log(' npx github:holistis/agent-receipt-verify kanon')
53
+ console.log(' cross-check our independent EIP-3009 verifier against the ten published')
54
+ console.log(' vectors of iamonuwa/kanon, offline, no account and no key.')
55
+ }
56
+
57
+ // Het argument bepaalt welke module draait. De modules zijn ook los te draaien en te
58
+ // importeren; dit bestand voegt alleen een ingang toe, geen tweede implementatie,
59
+ // want twee plekken die hetzelfde doen lopen altijd uit elkaar.
60
+ const onderdelen = {
61
+ mcp: '../src/mcp-server.mjs',
62
+ census: '../src/x402-discovery-census.mjs',
63
+ link: '../src/x402-link-onchain.mjs',
64
+ inam: '../src/inam-countersigned.mjs',
65
+ liveness: '../src/x402-index-liveness.mjs',
66
+ kanon: '../src/eip3009-autorisatie.mjs',
67
+ }
68
+
69
+ if (!eerste || eerste === '--help' || eerste === '-h' || eerste === 'help') {
70
+ gebruik()
71
+ process.exitCode = eerste ? 0 : 2
72
+ } else if (onderdelen[eerste]) {
73
+ // De doorgegeven argumenten zo neerzetten dat de module zijn eigen cli-tak kiest:
74
+ // die kijkt naar process.argv[1], dus die moet naar de module zelf wijzen.
75
+ const { fileURLToPath } = await import('node:url')
76
+ const pad = fileURLToPath(new URL(onderdelen[eerste], import.meta.url))
77
+ process.argv = [process.argv[0], pad, ...args.slice(1)]
78
+ await import(onderdelen[eerste])
79
+ } else if (/^https?:\/\//.test(eerste)) {
80
+ const { fileURLToPath } = await import('node:url')
81
+ const pad = fileURLToPath(new URL('../src/x402-conformance-check.mjs', import.meta.url))
82
+ process.argv = [process.argv[0], pad, ...args]
83
+ await import('../src/x402-conformance-check.mjs')
84
+ } else {
85
+ console.log('onbekend: ' + eerste)
86
+ console.log('')
87
+ gebruik()
88
+ process.exitCode = 2
89
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "note": "The second of two payments with IDENTICAL terms: same payer, same payee, same amount, same resource, minutes apart. Without a commitment these two receipts cannot be told apart. Each one still resolves to its own settlement.",
3
+ "source": "A real payment to https://api.tokenizen.nl/api/paid/verify-ownership on Base Sepolia, 2026-10-16. Buyer and seller are both us.",
4
+ "network": "eip155:84532",
5
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
6
+ "authorizationSalt": "0x63072ea656fe922bf04c9cefc4275314d695fff01e5b57f6628c8df0252cc5dc",
7
+ "receipt": {
8
+ "format": "eip712",
9
+ "payload": {
10
+ "version": 2,
11
+ "network": "eip155:84532",
12
+ "resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
13
+ "payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
14
+ "issuedAt": 1792187518,
15
+ "transaction": "",
16
+ "authorizationMethod": "eip3009",
17
+ "authorizationCommitment": "0xfecee93e1aec7d6858a218bf324f23e2a4eed07925d8d4542b9662c5026e3d22"
18
+ },
19
+ "signature": "0x02f5ba0d2b7928a2dbb1d8845b664a79d38eebcd0a2c4532ed3c6b922051bf8d47c8aafb0520a875a708e3ed480d84a537962ea90d66ce2ee3811f4229aa8b401b"
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "note": "A third payment on the same terms, so an independent implementation has more than one case to reach the same settlement on. Holds only what section 5.6.4 says a later verifier has: the receipt, its salt, and the asset that was paid.",
3
+ "source": "A real payment to https://api.tokenizen.nl/api/paid/verify-ownership on Base Sepolia, 2026-10-17. Buyer and seller are both us.",
4
+ "network": "eip155:84532",
5
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
6
+ "authorizationSalt": "0x349ddb90f38a3119817c5fc9cc253a2c20bcbe44aa845302df76889082b314d8",
7
+ "receipt": {
8
+ "format": "eip712",
9
+ "payload": {
10
+ "version": 2,
11
+ "network": "eip155:84532",
12
+ "resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
13
+ "payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
14
+ "issuedAt": 1792188211,
15
+ "transaction": "",
16
+ "authorizationMethod": "eip3009",
17
+ "authorizationCommitment": "0xea8dd34b8beac90f021a4e8d893a689c25249f86947770c8e6647c1cde206008"
18
+ },
19
+ "signature": "0x5b3a70cce6978ab44e51828b5ebbc55522e98b015045d94559d369420698da3b5309b28050dcb00ce14d65c8b230a273f8ff5ebff6410c6ae2a308e422ed5a611b"
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "note": "The first of two payments with IDENTICAL terms. This file holds exactly what section 5.6.4 says a later verifier has: the receipt, its salt, and the asset that was paid. The settlement nonce and transaction hash are deliberately NOT here; finding them is what the tool has to do.",
3
+ "source": "A real payment to https://api.tokenizen.nl/api/paid/verify-ownership on Base Sepolia, 2026-10-16. Buyer and seller are both us, so this proves the chain closes end to end, not that anyone else uses it.",
4
+ "network": "eip155:84532",
5
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
6
+ "authorizationSalt": "0xea85b2473363fd8fff1ccdd7f8638be8b47c7c05023e3e2e3f81bebb085ba2ad",
7
+ "receipt": {
8
+ "format": "eip712",
9
+ "payload": {
10
+ "version": 2,
11
+ "network": "eip155:84532",
12
+ "resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
13
+ "payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
14
+ "issuedAt": 1792187030,
15
+ "transaction": "",
16
+ "authorizationMethod": "eip3009",
17
+ "authorizationCommitment": "0x5c35a61a09a566a9c284fad18a70ac2ddfff34d01d2a3ce776ce94e8d5b38ff8"
18
+ },
19
+ "signature": "0x78615cb22986af0aad6e08b74ca78773bb58ece21660e84e4b35b3e5c1841b0075238ca9bfdf0f969ad3f2903af8f391fafd1b3fc7ebbf1fb5c949a3948fccde1c"
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "agent-receipt-verify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Checks x402 endpoint conformance before an agent pays, and links the receipt to its settlement.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "scripts": {
11
+ "test": "node test/interop.mjs && node test/canonical.mjs && node test/link-onchain.mjs && node test/profiel.mjs && node test/eip3009.mjs && node test/conformance.mjs && node test/jws-offer.mjs && node test/census.mjs && node test/poortwachters.mjs && node test/metingen-bundel.mjs && node test/mcp-server.mjs && node test/cli.mjs && node test/bundel.mjs",
12
+ "link-example": "node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia.json && node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia-2.json && node src/x402-link-onchain.mjs --receipt=examples/receipt-live-base-sepolia-3.json",
13
+ "conformance": "node src/x402-conformance-check.mjs",
14
+ "bench": "node bench/robustness.mjs",
15
+ "census": "node src/x402-discovery-census.mjs",
16
+ "liveness": "node src/x402-index-liveness.mjs",
17
+ "poortwachters": "node src/x402-poortwachters.mjs",
18
+ "metingen-bundel": "node src/x402-metingen-bundel.mjs",
19
+ "mcp": "node src/mcp-server.mjs",
20
+ "bundel": "node tools/bundel-bouw.mjs build/bundle",
21
+ "kanon": "node src/eip3009-autorisatie.mjs --vectoren"
22
+ },
23
+ "optionalDependencies": {
24
+ "ethers": "^6.17.0"
25
+ },
26
+ "bin": {
27
+ "agent-receipt-verify": "bin/cli.mjs"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "src/",
32
+ "profiles/",
33
+ "vectors/",
34
+ "examples/",
35
+ "NOTICE",
36
+ "CONFORMANCE.md",
37
+ "README.md",
38
+ "tools/"
39
+ ],
40
+ "author": "Abdellah Ouadoudi (https://github.com/holistis)",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/holistis/agent-receipt-verify.git"
44
+ },
45
+ "homepage": "https://github.com/holistis/agent-receipt-verify#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/holistis/agent-receipt-verify/issues"
48
+ },
49
+ "keywords": [
50
+ "x402",
51
+ "mcp",
52
+ "model-context-protocol",
53
+ "conformance",
54
+ "receipts",
55
+ "agent-payments",
56
+ "eip-3009",
57
+ "erc-8004",
58
+ "base",
59
+ "verification"
60
+ ]
61
+ }
@@ -0,0 +1,115 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/holistis/agent-receipt-verify/blob/main/profiles/profile.schema.json",
4
+ "title": "Deployed-endpoint conformance profile",
5
+ "description": "A versioned, declarative description of what can be checked on someone else's deployed endpoint from the response it already returns. A profile names the clause each check comes from, the input the check needs, and what to report when the check cannot decide. It does not describe how to run the check; that is the runner's job, and the evaluator field is the name that binds the two.",
6
+ "type": "object",
7
+ "required": ["profile", "version", "published", "intent", "outcomes", "specs", "checks"],
8
+ "additionalProperties": true,
9
+ "properties": {
10
+ "profile": { "type": "string", "description": "Stable name of the profile, independent of its version." },
11
+ "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
12
+ "published": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "description": "A profile without a date cannot be told apart from a later one, and then a published number cannot be traced back to the ladder it was measured on." },
13
+ "intent": { "type": "string", "minLength": 20 },
14
+ "notMeasured": { "type": "string", "description": "What this profile deliberately says nothing about. A profile that only lists what it checks invites the reader to assume the rest was fine." },
15
+ "outcomes": {
16
+ "type": "object",
17
+ "description": "Exactly four, and each one described. Three is the common mistake: it collapses 'we did not try' into 'we could not decide', and a reader cannot then tell a gap in the runner from a defect in the endpoint.",
18
+ "required": ["pass", "fail", "skip", "inconclusive"],
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "pass": { "type": "string", "minLength": 10 },
22
+ "fail": { "type": "string", "minLength": 10 },
23
+ "skip": { "type": "string", "minLength": 10 },
24
+ "inconclusive": { "type": "string", "minLength": 10 }
25
+ }
26
+ },
27
+ "wireStates": { "type": "object", "description": "Optional. How a particular runner's own state names map onto the four outcomes, so a consumer does not have to read that runner's source." },
28
+ "specs": {
29
+ "type": "object",
30
+ "description": "Every specification this profile cites, pinned. A clause reference without a pinned version is a reference to whatever the document says today.",
31
+ "minProperties": 1,
32
+ "additionalProperties": {
33
+ "type": "object",
34
+ "required": ["repo", "path", "commit", "state"],
35
+ "additionalProperties": true,
36
+ "properties": {
37
+ "repo": { "type": "string" },
38
+ "path": { "type": "string" },
39
+ "commit": { "type": "string", "minLength": 7 },
40
+ "state": { "type": "string", "enum": ["merged on main", "proposed, not merged"] },
41
+ "pullRequest": { "type": "integer" },
42
+ "retrieved": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
43
+ "warning": { "type": "string" }
44
+ }
45
+ }
46
+ },
47
+ "relatedWork": { "type": "object", "description": "Optional. Other vector or profile sets this one references instead of restating." },
48
+ "checks": {
49
+ "type": "array",
50
+ "minItems": 1,
51
+ "items": {
52
+ "type": "object",
53
+ "required": ["id", "key", "level", "name", "requirement", "clause", "input", "assertion", "normative", "whenNotObservable"],
54
+ "additionalProperties": true,
55
+ "properties": {
56
+ "id": { "type": "string", "pattern": "^[A-Z]+-[A-Za-z0-9]+$" },
57
+ "key": { "type": "string", "description": "Short key a runner uses in its own output. Kept separate from id so a runner's existing output does not have to change when a profile is adopted." },
58
+ "level": { "type": "integer", "minimum": 0 },
59
+ "name": { "type": "string", "minLength": 5 },
60
+ "requirement": { "type": "string", "minLength": 20 },
61
+ "clause": {
62
+ "type": "array",
63
+ "minItems": 1,
64
+ "description": "Where the requirement comes from. Each entry names a key of specs, so the pin travels with the reference.",
65
+ "items": {
66
+ "type": "object",
67
+ "required": ["spec", "section"],
68
+ "additionalProperties": true,
69
+ "properties": {
70
+ "spec": { "type": "string" },
71
+ "section": { "type": "string", "minLength": 3 },
72
+ "quote": { "type": "string" }
73
+ }
74
+ }
75
+ },
76
+ "input": {
77
+ "type": "object",
78
+ "required": ["needs", "pays"],
79
+ "additionalProperties": true,
80
+ "properties": {
81
+ "needs": { "type": "array", "minItems": 1, "items": { "type": "string" } },
82
+ "requests": { "type": "integer", "minimum": 0 },
83
+ "pays": { "type": "boolean", "description": "Whether the check needs a real settlement. A passive profile must skip these rather than fail them." }
84
+ }
85
+ },
86
+ "assertion": {
87
+ "type": "object",
88
+ "required": ["evaluator", "describes"],
89
+ "additionalProperties": false,
90
+ "properties": {
91
+ "evaluator": { "type": "string", "description": "Name of the routine that decides this check. Deliberately a name and not an expression language: the two checks that matter here involve signature recovery and a chain read, and pretending those fit in a data expression would mean either a dishonest profile or an expression language nobody else implements." },
92
+ "describes": { "type": "string", "minLength": 20, "description": "What is actually asserted, in plain words, so a second implementation can be written from the profile alone." }
93
+ }
94
+ },
95
+ "normative": { "type": "string", "enum": ["MUST", "SHOULD"] },
96
+ "whenNotObservable": { "type": "string", "enum": ["skip", "inconclusive"] },
97
+ "inconclusiveWhen": { "type": "array", "items": { "type": "string" } },
98
+ "relatedVectors": {
99
+ "type": "array",
100
+ "items": {
101
+ "type": "object",
102
+ "required": ["set", "id"],
103
+ "additionalProperties": true,
104
+ "properties": {
105
+ "set": { "type": "string" },
106
+ "id": { "type": "string" },
107
+ "subject": { "type": "string" }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,178 @@
1
+ {
2
+ "$schema": "./profile.schema.json",
3
+ "profile": "x402-deployed-ladder",
4
+ "version": "1.0.0",
5
+ "published": "2026-10-17",
6
+ "intent": "Passive audit of a deployed third-party x402 endpoint from the response it already returns, without cooperation from its operator and without paying.",
7
+ "notMeasured": "Whether the party behind the endpoint is honest, whether the service does what it advertises, whether a price is fair, and anything that needs a settlement. See CONFORMANCE.md for the full list.",
8
+ "outcomes": {
9
+ "pass": "The check ran and the response satisfies it.",
10
+ "fail": "The check ran and the response does not satisfy it.",
11
+ "skip": "The check was deliberately not attempted, because this profile is passive and the check needs something a passive run will not do, such as paying.",
12
+ "inconclusive": "The check was attempted and could not decide, for example because a signature algorithm or a key location is outside what the runner supports. Never report this as fail: a runner's own gap is not a defect of the endpoint."
13
+ },
14
+ "wireStates": {
15
+ "note": "The reference runner in this repository emits three states for backward compatibility. This table maps them onto the four outcomes above, so a consumer of this profile can translate without reading our code.",
16
+ "HAALT": "pass",
17
+ "HAALT_NIET": "fail",
18
+ "ONGEMETEN": "skip when the check declares whenNotObservable skip, inconclusive otherwise"
19
+ },
20
+ "specs": {
21
+ "x402-http-v2": {
22
+ "repo": "x402-foundation/x402",
23
+ "path": "specs/transports-v2/http.md",
24
+ "commit": "faa1c694c66a",
25
+ "state": "merged on main",
26
+ "retrieved": "2026-10-17"
27
+ },
28
+ "offer-receipt": {
29
+ "repo": "x402-foundation/x402",
30
+ "path": "specs/extensions/extension-offer-and-receipt.md",
31
+ "commit": "faa1c694c66a",
32
+ "state": "merged on main",
33
+ "retrieved": "2026-10-17"
34
+ },
35
+ "offer-receipt-commitment": {
36
+ "repo": "x402-foundation/x402",
37
+ "path": "specs/extensions/extension-offer-and-receipt.md",
38
+ "commit": "0e492d6",
39
+ "state": "proposed, not merged",
40
+ "pullRequest": 3827,
41
+ "retrieved": "2026-10-17",
42
+ "warning": "Sections 5.6 through 5.6.5 do not exist in the version on main. They exist only on this pull request, which is open. The pinned commit is four commits ahead of main and is fetchable, but the section numbers can still move before it merges. Any check referencing this spec entry is measuring against a proposal, and a result carrying it should say so."
43
+ }
44
+ },
45
+ "relatedWork": {
46
+ "x402-core-vectors": {
47
+ "repo": "x402-foundation/x402",
48
+ "path": "conformance/vectors/x402-core/",
49
+ "pullRequest": 2776,
50
+ "state": "proposed, not merged",
51
+ "note": "That set names a requirement and the request that exercises it, for an implementer proving their own implementation. Its expected field is the same sentence on every vector, so it carries no machine-checkable expectation. This profile references its ids where the subject overlaps rather than restating the requirement, and adds the three things a passive run needs: which input the check requires, what is actually asserted, and what to report when the check cannot decide. Raised as a question in issue 3396."
52
+ }
53
+ },
54
+ "checks": [
55
+ {
56
+ "id": "ARV-L0",
57
+ "key": "t0",
58
+ "level": 0,
59
+ "name": "asks for payment the way x402 says",
60
+ "requirement": "HTTP 402 carrying accepts[] with scheme, network, payTo and an amount, in the body or in the Payment-Required header",
61
+ "clause": [
62
+ { "spec": "x402-http-v2", "section": "Payment Required Signaling" },
63
+ { "spec": "x402-http-v2", "section": "Header Summary" }
64
+ ],
65
+ "input": { "needs": ["response status", "response headers", "response body"], "requests": 1, "pays": false },
66
+ "assertion": {
67
+ "evaluator": "statusAndAccepts",
68
+ "describes": "status is 402, and an accepts entry is reachable either in the JSON body or base64 in the Payment-Required header, carrying all four fields"
69
+ },
70
+ "normative": "MUST",
71
+ "whenNotObservable": "inconclusive",
72
+ "relatedVectors": [
73
+ { "set": "x402-core-vectors", "id": "X4-001", "subject": "402 payment challenge headers present" }
74
+ ]
75
+ },
76
+ {
77
+ "id": "ARV-L1",
78
+ "key": "t1",
79
+ "level": 1,
80
+ "name": "declares the offer-receipt extension",
81
+ "requirement": "the 402 carries an offer-receipt entry in extensions",
82
+ "clause": [
83
+ { "spec": "offer-receipt", "section": "4.1 Placement", "quote": "extensions[\"offer-receipt\"].info.offers[]" }
84
+ ],
85
+ "input": { "needs": ["the same 402 as ARV-L0"], "requests": 0, "pays": false },
86
+ "assertion": {
87
+ "evaluator": "extensionDeclared",
88
+ "describes": "extensions carries an offer-receipt key, counted from either the object form or the array form"
89
+ },
90
+ "normative": "SHOULD",
91
+ "whenNotObservable": "inconclusive",
92
+ "relatedVectors": []
93
+ },
94
+ {
95
+ "id": "ARV-L2",
96
+ "key": "t2",
97
+ "level": 2,
98
+ "name": "signs its offer, and the signature verifies",
99
+ "requirement": "extensions[\"offer-receipt\"].info.offers[0] is a signed artifact whose signature verifies: either an eip712 artifact recovering to an address, or a jws with an EdDSA signature over the offer payload whose key resolves from its kid",
100
+ "clause": [
101
+ { "spec": "offer-receipt", "section": "3.1 Common Object Shape" },
102
+ { "spec": "offer-receipt", "section": "3.3 JWS Header Requirements" },
103
+ { "spec": "offer-receipt", "section": "4.3 EIP-712 Types for Offer (Normative Schema)" },
104
+ { "spec": "offer-receipt", "section": "4.5 Offer Verification" },
105
+ { "spec": "offer-receipt", "section": "4.5.1 Signer Authorization" }
106
+ ],
107
+ "input": { "needs": ["the same 402 as ARV-L0", "for a jws offer with a did:web kid: one https fetch of that did document"], "requests": 0, "pays": false },
108
+ "assertion": {
109
+ "evaluator": "offerSignatureVerifies",
110
+ "describes": "the artifact's format is recognised and its signature verifies over the offer payload"
111
+ },
112
+ "normative": "SHOULD",
113
+ "whenNotObservable": "inconclusive",
114
+ "inconclusiveWhen": [
115
+ "the signature algorithm is not one this runner implements",
116
+ "the key cannot be resolved, for example a did:web document that does not answer or whose publicKeyMultibase is not multibase over multicodec 0xed01",
117
+ "the optional ethers dependency is absent, which is what an eip712 recovery needs"
118
+ ],
119
+ "relatedVectors": []
120
+ },
121
+ {
122
+ "id": "ARV-L3",
123
+ "key": "t3",
124
+ "level": 3,
125
+ "name": "emits a receipt that verifies",
126
+ "requirement": "after payment, the settlement response carries a receipt whose signature recovers to the same signer",
127
+ "clause": [
128
+ { "spec": "offer-receipt", "section": "5.1 Placement", "quote": "extensions[\"offer-receipt\"].info.receipt" },
129
+ { "spec": "offer-receipt", "section": "5.5 Receipt Verification" }
130
+ ],
131
+ "input": { "needs": ["a settlement response from a paid request"], "requests": 1, "pays": true },
132
+ "assertion": {
133
+ "evaluator": "receiptSignatureVerifies",
134
+ "describes": "a receipt is present and its signature recovers to the signer that signed the offer"
135
+ },
136
+ "normative": "SHOULD",
137
+ "whenNotObservable": "skip",
138
+ "relatedVectors": []
139
+ },
140
+ {
141
+ "id": "ARV-L4",
142
+ "key": "t4",
143
+ "level": 4,
144
+ "name": "receipt is version 2 and the salt arrives where 5.6.3 says",
145
+ "requirement": "payload.version is 2 with both commitment fields, and the salt is at extensions[\"offer-receipt\"].info.authorizationSalt",
146
+ "clause": [
147
+ { "spec": "offer-receipt-commitment", "section": "5.6 Authorization Commitment (receipt version 2)" },
148
+ { "spec": "offer-receipt-commitment", "section": "5.6.3 Salt" }
149
+ ],
150
+ "input": { "needs": ["a settlement response from a paid request"], "requests": 0, "pays": true },
151
+ "assertion": {
152
+ "evaluator": "receiptVersionTwoAndSaltPlacement",
153
+ "describes": "payload.version is 2, both commitment fields are present, and the salt sits outside the signed payload at the declared location"
154
+ },
155
+ "normative": "MUST",
156
+ "whenNotObservable": "skip",
157
+ "relatedVectors": []
158
+ },
159
+ {
160
+ "id": "ARV-L5",
161
+ "key": "t5",
162
+ "level": 5,
163
+ "name": "the commitment opens against the chain to exactly one settlement",
164
+ "requirement": "section 5.6.4 run against live chain state resolves to exactly one AuthorizationUsed nonce",
165
+ "clause": [
166
+ { "spec": "offer-receipt-commitment", "section": "5.6.4 Linking a receipt to its settlement (eip3009)" }
167
+ ],
168
+ "input": { "needs": ["a receipt", "its authorization salt", "the asset that was paid", "a chain read"], "requests": 0, "pays": true },
169
+ "assertion": {
170
+ "evaluator": "commitmentOpensOnExactlyOneNonce",
171
+ "describes": "recomputing the commitment over candidate AuthorizationUsed nonces opens on exactly one; zero and more than one both fail closed"
172
+ },
173
+ "normative": "MUST",
174
+ "whenNotObservable": "skip",
175
+ "relatedVectors": []
176
+ }
177
+ ]
178
+ }