agent-receipt-verify 0.2.3 → 0.2.4
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 +38 -3
- package/bin/cli.mjs +10 -0
- package/package.json +5 -3
- package/src/settlement-link-vectors.mjs +124 -11
- package/src/verify-delivery.mjs +507 -0
- package/src/x402-discovery-census.mjs +24 -1
- package/src/x402-offer-receipt.mjs +49 -3
- package/tools/naar-kanon-vorm.mjs +172 -0
- package/tools/pack-inhoud.mjs +45 -1
- package/vectors/settlement-link-kanon/README.md +69 -0
- package/vectors/settlement-link-kanon/find-candidates-onchain.json +43 -0
- package/vectors/settlement-link-kanon/no-candidates-not-measured.json +42 -0
- package/vectors/settlement-link-kanon/opens-live-2.json +47 -0
- package/vectors/settlement-link-kanon/opens-live-3.json +47 -0
- package/vectors/settlement-link-kanon/opens-live.json +47 -0
- package/vectors/settlement-link-kanon/two-identical-payments.json +76 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live-2.json +46 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live-3.json +46 -0
- package/vectors/settlement-link-kanon/wrong-nonce-live.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live-2.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live-3.json +46 -0
- package/vectors/settlement-link-kanon/wrong-salt-live.json +46 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Zet onze afrekening-vectoren om naar de vorm van de kanon-vectoren.
|
|
3
|
+
//
|
|
4
|
+
// WAAROM DIT BESTAAT, en het is een verzoek en geen eigen inval. De auteur van de
|
|
5
|
+
// kanon-vectoren schreef op 2026-10-17 in x402#3396: "if you want receipt-to-settlement
|
|
6
|
+
// vectors in Kanon's shape, I'm open to that, with the part that needs a chain read
|
|
7
|
+
// marked as such." Dit is dat, met die markering erin.
|
|
8
|
+
//
|
|
9
|
+
// WAT HIER WEL EN NIET VERTAALD KAN WORDEN, en dat tweede is het interessante deel.
|
|
10
|
+
// Zijn `expected` is `{ valid: boolean, reason_code: string }`. Twee van onze vier
|
|
11
|
+
// uitkomsten passen daar precies in. De andere twee niet, en dat is geen slordigheid
|
|
12
|
+
// van hem maar het gat dat hij in dezelfde reactie zelf benoemde en zei te willen
|
|
13
|
+
// dichten. Daarom laat dit script die twee gevallen ZICHTBAAR open staan in plaats van
|
|
14
|
+
// ze in `valid: false` te duwen:
|
|
15
|
+
//
|
|
16
|
+
// LINKED -> valid true, reason_code LINKED
|
|
17
|
+
// NOT_LINKED -> valid false, reason_code NOT_LINKED
|
|
18
|
+
// AMBIGUOUS -> valid false, reason_code AMBIGUOUS
|
|
19
|
+
// NOT_MEASURED -> GEEN valid. `measurable: false` plus een reason_code.
|
|
20
|
+
//
|
|
21
|
+
// Die laatste regel is het hele punt van de omzetting. Een bonnetje waarvan de
|
|
22
|
+
// kandidatenlijst niet opgehaald kon worden is NIET afgekeurd, en het in `valid: false`
|
|
23
|
+
// schrijven zou een eerlijke verkoper beschuldigen omdat onze RPC haperde. Liever een
|
|
24
|
+
// veld dat zijn validator nog niet kent dan een onwaarheid die hij wel kan lezen.
|
|
25
|
+
//
|
|
26
|
+
// EN DE GRENS VAN DEZE VERTALING, want die hoort erbij. Zijn corpus dekt de autorisatie
|
|
27
|
+
// van de KOPER voor de afrekening: geldigheidsvenster, malleability, domein-replay,
|
|
28
|
+
// nonce-hergebruik, bedrag. Dit corpus dekt de andere kant: opent het gezouten
|
|
29
|
+
// commitment in een ONDERTEKEND bonnetje op precies een afrekening op de keten. Die
|
|
30
|
+
// twee overlappen nergens, dus dit is een aanvulling en geen samenvoeging.
|
|
31
|
+
//
|
|
32
|
+
// Gebruik:
|
|
33
|
+
// node tools/naar-kanon-vorm.mjs schrijft naar vectors/settlement-link-kanon
|
|
34
|
+
// node tools/naar-kanon-vorm.mjs --controleer schrijft niets, meldt alleen afwijkingen
|
|
35
|
+
|
|
36
|
+
import fs from 'node:fs'
|
|
37
|
+
import path from 'node:path'
|
|
38
|
+
import { fileURLToPath } from 'node:url'
|
|
39
|
+
|
|
40
|
+
const WORTEL = fileURLToPath(new URL('..', import.meta.url))
|
|
41
|
+
export const BRONMAP = path.join(WORTEL, 'vectors', 'settlement-link')
|
|
42
|
+
export const DOELMAP = path.join(WORTEL, 'vectors', 'settlement-link-kanon')
|
|
43
|
+
|
|
44
|
+
/** Hun schema-versie, zoals die in elk van hun eigen vectoren staat. */
|
|
45
|
+
export const KANON_SCHEMA_VERSIE = '3.0.0'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* De vertaaltabel van onze uitkomst naar hun expected-blok.
|
|
49
|
+
*
|
|
50
|
+
* `valid` wordt met opzet WEGGELATEN bij NOT_MEASURED, niet op false gezet. Een
|
|
51
|
+
* ontbrekend veld is voor een lezer een vraag; een false is een beschuldiging.
|
|
52
|
+
*/
|
|
53
|
+
export function naarExpected(onze) {
|
|
54
|
+
const o = onze?.outcome
|
|
55
|
+
if (o === 'LINKED') {
|
|
56
|
+
return { valid: true, reason_code: 'LINKED' }
|
|
57
|
+
}
|
|
58
|
+
if (o === 'NOT_LINKED') {
|
|
59
|
+
return { valid: false, reason_code: 'NOT_LINKED' }
|
|
60
|
+
}
|
|
61
|
+
if (o === 'AMBIGUOUS') {
|
|
62
|
+
return { valid: false, reason_code: 'AMBIGUOUS' }
|
|
63
|
+
}
|
|
64
|
+
if (o === 'NOT_MEASURED') {
|
|
65
|
+
return {
|
|
66
|
+
measurable: false,
|
|
67
|
+
reason_code: 'NOT_MEASURED',
|
|
68
|
+
note:
|
|
69
|
+
'Deliberately carries no `valid`. The candidate set could not be obtained, so this says ' +
|
|
70
|
+
'nothing about the receipt. Writing valid:false here would blame an honest seller for a ' +
|
|
71
|
+
'failed chain read. This is the third-outcome gap discussed in x402#3396.',
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Zet een van onze vectoren om. Geeft null terug als de uitkomst niet te vertalen is. */
|
|
78
|
+
export function zetOm(v) {
|
|
79
|
+
const expected = naarExpected(v.expected)
|
|
80
|
+
if (!expected) return null
|
|
81
|
+
const uit = {
|
|
82
|
+
id: v.id,
|
|
83
|
+
schema_version: KANON_SCHEMA_VERSIE,
|
|
84
|
+
protocol: v.protocol ?? 'x402',
|
|
85
|
+
scheme: v.scheme ?? 'exact',
|
|
86
|
+
network: v.network,
|
|
87
|
+
asset_transfer_method: v.asset_transfer_method ?? 'eip3009',
|
|
88
|
+
// Een eigen veld, en het staat er omdat hun corpus deze as niet heeft. Hun vectoren
|
|
89
|
+
// gaan allemaal over de autorisatie VOOR de afrekening; deze gaan over het bonnetje
|
|
90
|
+
// NA de afrekening. Zonder dit veld zou een lezer de twee verzamelingen door elkaar
|
|
91
|
+
// halen zodra ze in een map staan.
|
|
92
|
+
verifies: 'receipt-to-settlement',
|
|
93
|
+
encodes: v.encodes,
|
|
94
|
+
provenance: v.provenance,
|
|
95
|
+
description: v.description,
|
|
96
|
+
// DE MARKERING DIE ERO M GEVRAAGD WERD. Hun schema heeft dit veld niet, dus het is
|
|
97
|
+
// additief: een loper die het niet kent negeert het en draait de vector gewoon, een
|
|
98
|
+
// loper die het wel kent weet dat er een RPC nodig is.
|
|
99
|
+
...(v.requires_chain_read ? { requires_chain_read: true } : {}),
|
|
100
|
+
input: v.input,
|
|
101
|
+
...(v.context ? { context: v.context } : {}),
|
|
102
|
+
expected,
|
|
103
|
+
}
|
|
104
|
+
return uit
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function laadBron(map = BRONMAP) {
|
|
108
|
+
if (!fs.existsSync(map)) return []
|
|
109
|
+
return fs.readdirSync(map).filter((f) => f.endsWith('.json')).sort()
|
|
110
|
+
.map((f) => ({ bestand: f, inhoud: JSON.parse(fs.readFileSync(path.join(map, f), 'utf8')) }))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const ditBestand = fileURLToPath(import.meta.url)
|
|
114
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(ditBestand)) {
|
|
115
|
+
const alleenControleren = process.argv.includes('--controleer')
|
|
116
|
+
const bron = laadBron()
|
|
117
|
+
if (!bron.length) {
|
|
118
|
+
console.error('no vectors found in vectors/settlement-link')
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!alleenControleren) fs.mkdirSync(DOELMAP, { recursive: true })
|
|
123
|
+
|
|
124
|
+
let geschreven = 0
|
|
125
|
+
let ongewijzigd = 0
|
|
126
|
+
let afwijkend = 0
|
|
127
|
+
let nietTeVertalen = 0
|
|
128
|
+
const telling = {}
|
|
129
|
+
|
|
130
|
+
for (const { bestand, inhoud } of bron) {
|
|
131
|
+
const om = zetOm(inhoud)
|
|
132
|
+
if (!om) {
|
|
133
|
+
nietTeVertalen++
|
|
134
|
+
console.log(' SKIP ' + bestand + ' (outcome ' + String(inhoud?.expected?.outcome) + ' has no translation)')
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
const code = om.expected.reason_code
|
|
138
|
+
telling[code] = (telling[code] ?? 0) + 1
|
|
139
|
+
const doel = path.join(DOELMAP, bestand)
|
|
140
|
+
const tekst = JSON.stringify(om, null, 2) + '\n'
|
|
141
|
+
if (alleenControleren) {
|
|
142
|
+
const bestaat = fs.existsSync(doel) ? fs.readFileSync(doel, 'utf8') : null
|
|
143
|
+
if (bestaat === tekst) ongewijzigd++
|
|
144
|
+
else { afwijkend++; console.log(' DIFFERS ' + bestand + (bestaat === null ? ' (missing)' : '')) }
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
fs.writeFileSync(doel, tekst)
|
|
148
|
+
geschreven++
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('')
|
|
152
|
+
if (alleenControleren) {
|
|
153
|
+
console.log(ongewijzigd + ' up to date, ' + afwijkend + ' differ, ' + nietTeVertalen + ' untranslatable')
|
|
154
|
+
process.exitCode = afwijkend === 0 ? 0 : 1
|
|
155
|
+
} else {
|
|
156
|
+
console.log(geschreven + ' vectors written to vectors/settlement-link-kanon')
|
|
157
|
+
}
|
|
158
|
+
console.log('')
|
|
159
|
+
console.log('reason codes used:')
|
|
160
|
+
for (const [k, v] of Object.entries(telling).sort((a, b) => b[1] - a[1])) {
|
|
161
|
+
console.log(' ' + String(v).padStart(3) + ' ' + k)
|
|
162
|
+
}
|
|
163
|
+
const ketenLees = bron.filter((b) => b.inhoud.requires_chain_read).length
|
|
164
|
+
console.log('')
|
|
165
|
+
console.log(ketenLees + ' of ' + bron.length + ' carry requires_chain_read:true, so a runner knows to expect an RPC.')
|
|
166
|
+
console.log('')
|
|
167
|
+
console.log('One honest note about the translation, and it is the reason this exists:')
|
|
168
|
+
console.log('Kanon expected is valid plus a reason_code. LINKED, NOT_LINKED and AMBIGUOUS map')
|
|
169
|
+
console.log('onto that cleanly. NOT_MEASURED does not, so those files carry measurable:false')
|
|
170
|
+
console.log('and no valid at all, rather than a valid:false that would read as a verdict about')
|
|
171
|
+
console.log('a seller when the truth is that our own chain read did not happen.')
|
|
172
|
+
}
|
package/tools/pack-inhoud.mjs
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// Exitcode 0 als er een lijst is, 1 als de inhoud niet te meten was.
|
|
29
29
|
|
|
30
30
|
import { execSync } from 'node:child_process'
|
|
31
|
+
import { readFileSync } from 'node:fs'
|
|
31
32
|
import path from 'node:path'
|
|
32
33
|
import { fileURLToPath } from 'node:url'
|
|
33
34
|
|
|
@@ -47,7 +48,13 @@ export function lijstUitPackUitvoer(ruw) {
|
|
|
47
48
|
throw new Error('npm pack gaf geen json terug: ' + String(e.message).slice(0, 80))
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// EEN LEGE LIJST IS GEEN LIJST. `Array.isArray([])` is waar en `[]` is in javascript
|
|
52
|
+
// ook waar, dus een entry met `files: []` kwam hier als geldige uitkomst door en gaf
|
|
53
|
+
// een lege bestandslijst terug. Dat is precies wat de koptekst hierboven belooft NIET
|
|
54
|
+
// te doen, en het is ook de storing die deze file se eigen geschiedenis beschrijft:
|
|
55
|
+
// twaalf controles die verdwenen terwijl de suite groen bleef. Een tarball met nul
|
|
56
|
+
// bestanden bestaat niet, dus dit is altijd een meetfout en nooit een uitkomst.
|
|
57
|
+
const uitEntry = (e) => (e && Array.isArray(e.files) && e.files.length ? e.files.map((f) => f.path) : null)
|
|
51
58
|
|
|
52
59
|
// Vorm 1: een array van entries (npm 11.11.0 en ouder).
|
|
53
60
|
if (Array.isArray(j)) {
|
|
@@ -83,6 +90,43 @@ export function pakInhoud({ cwd = WORTEL, draai = null } = {}) {
|
|
|
83
90
|
return lijstUitPackUitvoer(uit)
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wat de tarball MOET dragen, afgeleid uit de verzendtabel van de cli zelf.
|
|
96
|
+
*
|
|
97
|
+
* WAAROM AFGELEID EN NIET OPGESCHREVEN. De publicatiestap heette "de tarball moet NOTICE,
|
|
98
|
+
* LICENSE en alles waar de cli naar verwijst dragen" en toetste vijf vaste paden, terwijl
|
|
99
|
+
* de verzendtabel negen modules heeft. Zeven subcommando's werden dus nooit gecontroleerd.
|
|
100
|
+
* Zou `files` in package.json ooit `src/` kwijtraken, dan ging er een pakket uit waarvan
|
|
101
|
+
* elk subcommando bij de eerste aanroep crasht, en deze poort zou groen staan. Een lijst
|
|
102
|
+
* die met de hand wordt bijgehouden loopt per definitie achter op de tabel die hij
|
|
103
|
+
* bewaakt; deze leest die tabel.
|
|
104
|
+
*
|
|
105
|
+
* De drie vaste regels eronder staan er wel met de hand, en met opzet: NOTICE en LICENSE
|
|
106
|
+
* zijn een LICENTIEPLICHT (wij verspreiden testvectoren van derden onder Apache-2.0) en
|
|
107
|
+
* volgen uit geen enkele tabel. Het profielbestand staat erbij omdat de ladder het leest
|
|
108
|
+
* zonder dat een importregel dat verraadt.
|
|
109
|
+
*/
|
|
110
|
+
export function verwachteBestanden({ cliPad = null } = {}) {
|
|
111
|
+
const pad = cliPad ?? path.join(WORTEL, 'bin', 'cli.mjs')
|
|
112
|
+
const bron = readFileSync(pad, 'utf8')
|
|
113
|
+
const tabel = bron.match(/const onderdelen = \{([^}]*)\}/)
|
|
114
|
+
if (!tabel) {
|
|
115
|
+
throw new Error('de verzendtabel is niet te vinden in ' + pad + ', dus de verwachte inhoud is ONGEMETEN')
|
|
116
|
+
}
|
|
117
|
+
const modules = [...tabel[1].matchAll(/'([^']+\.mjs)'/g)].map((m) => m[1].replace(/^\.\.\//, ''))
|
|
118
|
+
if (!modules.length) {
|
|
119
|
+
throw new Error('de verzendtabel in ' + pad + ' bevat geen enkel modulepad, dus hier is iets stuk')
|
|
120
|
+
}
|
|
121
|
+
return [
|
|
122
|
+
'NOTICE',
|
|
123
|
+
'LICENSE',
|
|
124
|
+
'bin/cli.mjs',
|
|
125
|
+
'profiles/x402-deployed-ladder-v1.json',
|
|
126
|
+
...modules,
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
|
|
86
130
|
const ditBestand = fileURLToPath(import.meta.url)
|
|
87
131
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(ditBestand)) {
|
|
88
132
|
try {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# The settlement-link vectors, in Kanon's shape
|
|
2
|
+
|
|
3
|
+
This directory is generated. Do not edit the files by hand; run
|
|
4
|
+
`node tools/naar-kanon-vorm.mjs` and commit the result. `npm test` checks that what is
|
|
5
|
+
on disk is byte-identical to what the converter would write today, so a stale copy
|
|
6
|
+
cannot sit here unnoticed.
|
|
7
|
+
|
|
8
|
+
## Why it exists
|
|
9
|
+
|
|
10
|
+
The author of the Kanon vectors wrote in
|
|
11
|
+
[x402#3396](https://github.com/x402-foundation/x402/issues/3396) on 17 October 2026:
|
|
12
|
+
|
|
13
|
+
> if you want receipt-to-settlement vectors in Kanon's shape, I'm open to that, with the
|
|
14
|
+
> part that needs a chain read marked as such.
|
|
15
|
+
|
|
16
|
+
This is that. The source corpus lives in `../settlement-link` and is the same twelve
|
|
17
|
+
cases; these are the same cases in their field layout, with the chain-reading one
|
|
18
|
+
marked.
|
|
19
|
+
|
|
20
|
+
## What these cover, and what they do not
|
|
21
|
+
|
|
22
|
+
Kanon covers the buyer's authorization **before** settlement: validity window, signature
|
|
23
|
+
malleability, domain replay across chain and contract, nonce reuse, amount. These cover
|
|
24
|
+
the other end: does a salted commitment inside a **signed receipt** open onto exactly one
|
|
25
|
+
settlement on chain. The two sets nowhere overlap, which is why this is a complement and
|
|
26
|
+
not a merge, and why every file here carries `verifies: "receipt-to-settlement"`.
|
|
27
|
+
|
|
28
|
+
That also means Kanon's existing runner cannot execute these as they stand. Their input
|
|
29
|
+
is `payload.authorization`; ours is a receipt plus a salt plus an asset. The shape is
|
|
30
|
+
theirs so the two corpora can sit in one place and be read by one reader. The content is
|
|
31
|
+
a different axis, and pretending otherwise would waste somebody's afternoon.
|
|
32
|
+
|
|
33
|
+
## The two fields we added, both additive
|
|
34
|
+
|
|
35
|
+
- `verifies` — which axis the vector tests. Without it, two corpora in one directory are
|
|
36
|
+
indistinguishable.
|
|
37
|
+
- `requires_chain_read` — present and `true` on exactly one vector. The other eleven run
|
|
38
|
+
offline, with no account, no key and no network. A runner that does not know this field
|
|
39
|
+
ignores it; a runner that does knows to expect an RPC.
|
|
40
|
+
|
|
41
|
+
## The one place the shape does not fit, and it is the interesting one
|
|
42
|
+
|
|
43
|
+
Kanon's `expected` is `{ valid, reason_code }`. Three of our four outcomes map onto that
|
|
44
|
+
cleanly:
|
|
45
|
+
|
|
46
|
+
| our outcome | expected |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `LINKED` | `valid: true`, `reason_code: LINKED` |
|
|
49
|
+
| `NOT_LINKED` | `valid: false`, `reason_code: NOT_LINKED` |
|
|
50
|
+
| `AMBIGUOUS` | `valid: false`, `reason_code: AMBIGUOUS` |
|
|
51
|
+
| `NOT_MEASURED` | **no `valid` at all**, `measurable: false` |
|
|
52
|
+
|
|
53
|
+
The fourth has no slot, and we left the hole visible rather than filling it. A receipt
|
|
54
|
+
whose candidate set could not be obtained has not been rejected: our chain read did not
|
|
55
|
+
happen, and that says nothing about the receipt. Writing `valid: false` there would
|
|
56
|
+
blame an honest seller for our own failed RPC. A missing field is a question for a
|
|
57
|
+
reader; a false is an accusation.
|
|
58
|
+
|
|
59
|
+
That is the third-outcome gap the same thread discussed, and the Kanon author said in
|
|
60
|
+
the same comment he would look at adding an optional third outcome without weakening the
|
|
61
|
+
accept and reject cases. One file in this directory is what that gap looks like in
|
|
62
|
+
practice: `no-candidates-not-measured.json`.
|
|
63
|
+
|
|
64
|
+
## Provenance
|
|
65
|
+
|
|
66
|
+
The Kanon vectors themselves are in `../kanon`, pinned at commit `0d5a7461e996`,
|
|
67
|
+
Apache-2.0, redistributed unmodified with attribution. The vectors here are ours, built
|
|
68
|
+
from three real payments on Base Sepolia; the receipts, salts and nonces in them are real
|
|
69
|
+
and the commitments re-derive from the signed receipts rather than being copied in.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "x402-evm-settlement-link-find-candidates-onchain",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"protocol": "x402",
|
|
5
|
+
"scheme": "exact",
|
|
6
|
+
"network": "eip155:84532",
|
|
7
|
+
"asset_transfer_method": "eip3009",
|
|
8
|
+
"verifies": "receipt-to-settlement",
|
|
9
|
+
"encodes": "The chain-reading half of the walk, marked as such.",
|
|
10
|
+
"provenance": [
|
|
11
|
+
"x402 offer-receipt extension section 5.6.4, as proposed in pull request 3827 at commit 0e492d6 and not on main",
|
|
12
|
+
"EIP-3009"
|
|
13
|
+
],
|
|
14
|
+
"description": "The full section 5.6.4 walk, starting from only the receipt, the salt and the asset: find the nonces this payer consumed in a window, then open the commitment on exactly one. Requires an archive-capable RPC for Base Sepolia. Included so the chain-dependent half is named rather than hidden, but a runner without RPC access should skip it and report that, not fail it.",
|
|
15
|
+
"requires_chain_read": true,
|
|
16
|
+
"input": {
|
|
17
|
+
"receipt": {
|
|
18
|
+
"format": "eip712",
|
|
19
|
+
"payload": {
|
|
20
|
+
"version": 2,
|
|
21
|
+
"network": "eip155:84532",
|
|
22
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
23
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
24
|
+
"issuedAt": 1792187030,
|
|
25
|
+
"transaction": "",
|
|
26
|
+
"authorizationMethod": "eip3009",
|
|
27
|
+
"authorizationCommitment": "0x5c35a61a09a566a9c284fad18a70ac2ddfff34d01d2a3ce776ce94e8d5b38ff8"
|
|
28
|
+
},
|
|
29
|
+
"signature": "0x78615cb22986af0aad6e08b74ca78773bb58ece21660e84e4b35b3e5c1841b0075238ca9bfdf0f969ad3f2903af8f391fafd1b3fc7ebbf1fb5c949a3948fccde1c"
|
|
30
|
+
},
|
|
31
|
+
"authorizationSalt": "0xea85b2473363fd8fff1ccdd7f8638be8b47c7c05023e3e2e3f81bebb085ba2ad",
|
|
32
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
33
|
+
},
|
|
34
|
+
"context": {
|
|
35
|
+
"rpc": "any Base Sepolia endpoint that serves eth_getLogs over the window around the receipt issuedAt",
|
|
36
|
+
"expected_candidate_count": 2,
|
|
37
|
+
"note": "The candidate count is what the window actually produced on 2026-10-18 and will grow as this payer makes more payments; a runner should assert \"exactly one opens\", not the count."
|
|
38
|
+
},
|
|
39
|
+
"expected": {
|
|
40
|
+
"valid": true,
|
|
41
|
+
"reason_code": "LINKED"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "x402-evm-settlement-link-no-candidates-not-measured",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"protocol": "x402",
|
|
5
|
+
"scheme": "exact",
|
|
6
|
+
"network": "eip155:84532",
|
|
7
|
+
"asset_transfer_method": "eip3009",
|
|
8
|
+
"verifies": "receipt-to-settlement",
|
|
9
|
+
"encodes": "An incomplete check must be distinguishable from a failed one.",
|
|
10
|
+
"provenance": [
|
|
11
|
+
"x402 offer-receipt extension section 5.6.4, as proposed in pull request 3827 at commit 0e492d6 and not on main",
|
|
12
|
+
"EIP-3009"
|
|
13
|
+
],
|
|
14
|
+
"description": "No candidate set could be obtained, for example because no chain access was available or the lookup window produced nothing. The correct answer is NOT_MEASURED and explicitly not NOT_LINKED: a check that could not run is not a receipt that fails. This is the third outcome a two-valued vocabulary cannot express.",
|
|
15
|
+
"input": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"format": "eip712",
|
|
18
|
+
"payload": {
|
|
19
|
+
"version": 2,
|
|
20
|
+
"network": "eip155:84532",
|
|
21
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
22
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
23
|
+
"issuedAt": 1792187030,
|
|
24
|
+
"transaction": "",
|
|
25
|
+
"authorizationMethod": "eip3009",
|
|
26
|
+
"authorizationCommitment": "0x5c35a61a09a566a9c284fad18a70ac2ddfff34d01d2a3ce776ce94e8d5b38ff8"
|
|
27
|
+
},
|
|
28
|
+
"signature": "0x78615cb22986af0aad6e08b74ca78773bb58ece21660e84e4b35b3e5c1841b0075238ca9bfdf0f969ad3f2903af8f391fafd1b3fc7ebbf1fb5c949a3948fccde1c"
|
|
29
|
+
},
|
|
30
|
+
"authorizationSalt": "0xea85b2473363fd8fff1ccdd7f8638be8b47c7c05023e3e2e3f81bebb085ba2ad",
|
|
31
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
32
|
+
"candidates": null
|
|
33
|
+
},
|
|
34
|
+
"context": {
|
|
35
|
+
"note": "candidates is null rather than an empty list, and the two mean different things: null is \"we did not obtain a list\", empty is \"we obtained a list and it was empty\"."
|
|
36
|
+
},
|
|
37
|
+
"expected": {
|
|
38
|
+
"measurable": false,
|
|
39
|
+
"reason_code": "NOT_MEASURED",
|
|
40
|
+
"note": "Deliberately carries no `valid`. The candidate set could not be obtained, so this says nothing about the receipt. Writing valid:false here would blame an honest seller for a failed chain read. This is the third-outcome gap discussed in x402#3396."
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "x402-evm-settlement-link-opens-live-2",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"protocol": "x402",
|
|
5
|
+
"scheme": "exact",
|
|
6
|
+
"network": "eip155:84532",
|
|
7
|
+
"asset_transfer_method": "eip3009",
|
|
8
|
+
"verifies": "receipt-to-settlement",
|
|
9
|
+
"encodes": "Receipt-to-settlement binding verified offline against a supplied candidate set.",
|
|
10
|
+
"provenance": [
|
|
11
|
+
"x402 offer-receipt extension section 5.6.4, as proposed in pull request 3827 at commit 0e492d6 and not on main",
|
|
12
|
+
"EIP-3009"
|
|
13
|
+
],
|
|
14
|
+
"description": "A version 2 receipt whose authorizationCommitment opens on exactly one of the candidate nonces supplied with it. No chain access is needed because the candidates are given; this is the arithmetic half of section 5.6.4.",
|
|
15
|
+
"input": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"format": "eip712",
|
|
18
|
+
"payload": {
|
|
19
|
+
"version": 2,
|
|
20
|
+
"network": "eip155:84532",
|
|
21
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
22
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
23
|
+
"issuedAt": 1792187518,
|
|
24
|
+
"transaction": "",
|
|
25
|
+
"authorizationMethod": "eip3009",
|
|
26
|
+
"authorizationCommitment": "0xfecee93e1aec7d6858a218bf324f23e2a4eed07925d8d4542b9662c5026e3d22"
|
|
27
|
+
},
|
|
28
|
+
"signature": "0x02f5ba0d2b7928a2dbb1d8845b664a79d38eebcd0a2c4532ed3c6b922051bf8d47c8aafb0520a875a708e3ed480d84a537962ea90d66ce2ee3811f4229aa8b401b"
|
|
29
|
+
},
|
|
30
|
+
"authorizationSalt": "0x63072ea656fe922bf04c9cefc4275314d695fff01e5b57f6628c8df0252cc5dc",
|
|
31
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
32
|
+
"candidates": [
|
|
33
|
+
{
|
|
34
|
+
"authorizer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
35
|
+
"nonce": "0x515895dfac6f148a4cfdf5c4f8266b7859dbd6534cdf77104b1be9f484eb5005"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"context": {
|
|
40
|
+
"note": "Real payment on Base Sepolia to https://api.tokenizen.nl/api/paid/verify-ownership. Buyer and seller are the same party, which proves the chain closes end to end and nothing about adoption.",
|
|
41
|
+
"settlement_transaction": "0x2898d0ceb97ddf60af98e19c94fc0adddbdf1e64f9032e62454085e37acfe36b"
|
|
42
|
+
},
|
|
43
|
+
"expected": {
|
|
44
|
+
"valid": true,
|
|
45
|
+
"reason_code": "LINKED"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "x402-evm-settlement-link-opens-live-3",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"protocol": "x402",
|
|
5
|
+
"scheme": "exact",
|
|
6
|
+
"network": "eip155:84532",
|
|
7
|
+
"asset_transfer_method": "eip3009",
|
|
8
|
+
"verifies": "receipt-to-settlement",
|
|
9
|
+
"encodes": "Receipt-to-settlement binding verified offline against a supplied candidate set.",
|
|
10
|
+
"provenance": [
|
|
11
|
+
"x402 offer-receipt extension section 5.6.4, as proposed in pull request 3827 at commit 0e492d6 and not on main",
|
|
12
|
+
"EIP-3009"
|
|
13
|
+
],
|
|
14
|
+
"description": "A version 2 receipt whose authorizationCommitment opens on exactly one of the candidate nonces supplied with it. No chain access is needed because the candidates are given; this is the arithmetic half of section 5.6.4.",
|
|
15
|
+
"input": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"format": "eip712",
|
|
18
|
+
"payload": {
|
|
19
|
+
"version": 2,
|
|
20
|
+
"network": "eip155:84532",
|
|
21
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
22
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
23
|
+
"issuedAt": 1792188211,
|
|
24
|
+
"transaction": "",
|
|
25
|
+
"authorizationMethod": "eip3009",
|
|
26
|
+
"authorizationCommitment": "0xea8dd34b8beac90f021a4e8d893a689c25249f86947770c8e6647c1cde206008"
|
|
27
|
+
},
|
|
28
|
+
"signature": "0x5b3a70cce6978ab44e51828b5ebbc55522e98b015045d94559d369420698da3b5309b28050dcb00ce14d65c8b230a273f8ff5ebff6410c6ae2a308e422ed5a611b"
|
|
29
|
+
},
|
|
30
|
+
"authorizationSalt": "0x349ddb90f38a3119817c5fc9cc253a2c20bcbe44aa845302df76889082b314d8",
|
|
31
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
32
|
+
"candidates": [
|
|
33
|
+
{
|
|
34
|
+
"authorizer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
35
|
+
"nonce": "0x4b133bbec3ce0d8177a9010784faedb459f04384b45316bb526a5f96c90a70de"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"context": {
|
|
40
|
+
"note": "Real payment on Base Sepolia to https://api.tokenizen.nl/api/paid/verify-ownership. Buyer and seller are the same party, which proves the chain closes end to end and nothing about adoption.",
|
|
41
|
+
"settlement_transaction": "0x2a9e95d63a5af8210767baa69143b364dad51caffc29a152c77e03bc508d7199"
|
|
42
|
+
},
|
|
43
|
+
"expected": {
|
|
44
|
+
"valid": true,
|
|
45
|
+
"reason_code": "LINKED"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "x402-evm-settlement-link-opens-live",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"protocol": "x402",
|
|
5
|
+
"scheme": "exact",
|
|
6
|
+
"network": "eip155:84532",
|
|
7
|
+
"asset_transfer_method": "eip3009",
|
|
8
|
+
"verifies": "receipt-to-settlement",
|
|
9
|
+
"encodes": "Receipt-to-settlement binding verified offline against a supplied candidate set.",
|
|
10
|
+
"provenance": [
|
|
11
|
+
"x402 offer-receipt extension section 5.6.4, as proposed in pull request 3827 at commit 0e492d6 and not on main",
|
|
12
|
+
"EIP-3009"
|
|
13
|
+
],
|
|
14
|
+
"description": "A version 2 receipt whose authorizationCommitment opens on exactly one of the candidate nonces supplied with it. No chain access is needed because the candidates are given; this is the arithmetic half of section 5.6.4.",
|
|
15
|
+
"input": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"format": "eip712",
|
|
18
|
+
"payload": {
|
|
19
|
+
"version": 2,
|
|
20
|
+
"network": "eip155:84532",
|
|
21
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
22
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
23
|
+
"issuedAt": 1792187030,
|
|
24
|
+
"transaction": "",
|
|
25
|
+
"authorizationMethod": "eip3009",
|
|
26
|
+
"authorizationCommitment": "0x5c35a61a09a566a9c284fad18a70ac2ddfff34d01d2a3ce776ce94e8d5b38ff8"
|
|
27
|
+
},
|
|
28
|
+
"signature": "0x78615cb22986af0aad6e08b74ca78773bb58ece21660e84e4b35b3e5c1841b0075238ca9bfdf0f969ad3f2903af8f391fafd1b3fc7ebbf1fb5c949a3948fccde1c"
|
|
29
|
+
},
|
|
30
|
+
"authorizationSalt": "0xea85b2473363fd8fff1ccdd7f8638be8b47c7c05023e3e2e3f81bebb085ba2ad",
|
|
31
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
32
|
+
"candidates": [
|
|
33
|
+
{
|
|
34
|
+
"authorizer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
35
|
+
"nonce": "0x1120412c78ea1b2e8bf9eacfc3264c71a23ecd9b1fa579eb16030693344d5dfe"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"context": {
|
|
40
|
+
"note": "Real payment on Base Sepolia to https://api.tokenizen.nl/api/paid/verify-ownership. Buyer and seller are the same party, which proves the chain closes end to end and nothing about adoption.",
|
|
41
|
+
"settlement_transaction": "0xed87b09bfb20a8c8b69908e163fcea6432d193d389640c0465afcb5561dfeb30"
|
|
42
|
+
},
|
|
43
|
+
"expected": {
|
|
44
|
+
"valid": true,
|
|
45
|
+
"reason_code": "LINKED"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "x402-evm-settlement-link-two-identical-payments",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"protocol": "x402",
|
|
5
|
+
"scheme": "exact",
|
|
6
|
+
"network": "eip155:84532",
|
|
7
|
+
"asset_transfer_method": "eip3009",
|
|
8
|
+
"verifies": "receipt-to-settlement",
|
|
9
|
+
"encodes": "Disambiguation of two otherwise indistinguishable payments.",
|
|
10
|
+
"provenance": [
|
|
11
|
+
"x402 offer-receipt extension section 5.6.4, as proposed in pull request 3827 at commit 0e492d6 and not on main",
|
|
12
|
+
"EIP-3009"
|
|
13
|
+
],
|
|
14
|
+
"description": "Two payments with identical terms (same payer, same asset, same amount, same resource) each open on their own settlement and on no other. Given both candidate sets together, each receipt selects exactly one.",
|
|
15
|
+
"input": {
|
|
16
|
+
"receipts": [
|
|
17
|
+
{
|
|
18
|
+
"receipt": {
|
|
19
|
+
"format": "eip712",
|
|
20
|
+
"payload": {
|
|
21
|
+
"version": 2,
|
|
22
|
+
"network": "eip155:84532",
|
|
23
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
24
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
25
|
+
"issuedAt": 1792187030,
|
|
26
|
+
"transaction": "",
|
|
27
|
+
"authorizationMethod": "eip3009",
|
|
28
|
+
"authorizationCommitment": "0x5c35a61a09a566a9c284fad18a70ac2ddfff34d01d2a3ce776ce94e8d5b38ff8"
|
|
29
|
+
},
|
|
30
|
+
"signature": "0x78615cb22986af0aad6e08b74ca78773bb58ece21660e84e4b35b3e5c1841b0075238ca9bfdf0f969ad3f2903af8f391fafd1b3fc7ebbf1fb5c949a3948fccde1c"
|
|
31
|
+
},
|
|
32
|
+
"authorizationSalt": "0xea85b2473363fd8fff1ccdd7f8638be8b47c7c05023e3e2e3f81bebb085ba2ad",
|
|
33
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"receipt": {
|
|
37
|
+
"format": "eip712",
|
|
38
|
+
"payload": {
|
|
39
|
+
"version": 2,
|
|
40
|
+
"network": "eip155:84532",
|
|
41
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
42
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
43
|
+
"issuedAt": 1792187518,
|
|
44
|
+
"transaction": "",
|
|
45
|
+
"authorizationMethod": "eip3009",
|
|
46
|
+
"authorizationCommitment": "0xfecee93e1aec7d6858a218bf324f23e2a4eed07925d8d4542b9662c5026e3d22"
|
|
47
|
+
},
|
|
48
|
+
"signature": "0x02f5ba0d2b7928a2dbb1d8845b664a79d38eebcd0a2c4532ed3c6b922051bf8d47c8aafb0520a875a708e3ed480d84a537962ea90d66ce2ee3811f4229aa8b401b"
|
|
49
|
+
},
|
|
50
|
+
"authorizationSalt": "0x63072ea656fe922bf04c9cefc4275314d695fff01e5b57f6628c8df0252cc5dc",
|
|
51
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"candidates": [
|
|
55
|
+
{
|
|
56
|
+
"authorizer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
57
|
+
"nonce": "0x1120412c78ea1b2e8bf9eacfc3264c71a23ecd9b1fa579eb16030693344d5dfe"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"authorizer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
61
|
+
"nonce": "0x515895dfac6f148a4cfdf5c4f8266b7859dbd6534cdf77104b1be9f484eb5005"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"context": {
|
|
66
|
+
"note": "Both are real payments on Base Sepolia with the same terms. Without the salt these two could not be told apart by any later reader.",
|
|
67
|
+
"settlements": [
|
|
68
|
+
"0xed87b09bfb20a8c8b69908e163fcea6432d193d389640c0465afcb5561dfeb30",
|
|
69
|
+
"0x2898d0ceb97ddf60af98e19c94fc0adddbdf1e64f9032e62454085e37acfe36b"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"expected": {
|
|
73
|
+
"valid": true,
|
|
74
|
+
"reason_code": "LINKED"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "x402-evm-settlement-link-wrong-nonce-live-2",
|
|
3
|
+
"schema_version": "3.0.0",
|
|
4
|
+
"protocol": "x402",
|
|
5
|
+
"scheme": "exact",
|
|
6
|
+
"network": "eip155:84532",
|
|
7
|
+
"asset_transfer_method": "eip3009",
|
|
8
|
+
"verifies": "receipt-to-settlement",
|
|
9
|
+
"encodes": "A candidate that is not this payment must not satisfy the commitment.",
|
|
10
|
+
"provenance": [
|
|
11
|
+
"x402 offer-receipt extension section 5.6.4, as proposed in pull request 3827 at commit 0e492d6 and not on main",
|
|
12
|
+
"EIP-3009"
|
|
13
|
+
],
|
|
14
|
+
"description": "The correct receipt and salt, but a candidate nonce that belongs to a different settlement by the same payer. The commitment must not open on it.",
|
|
15
|
+
"input": {
|
|
16
|
+
"receipt": {
|
|
17
|
+
"format": "eip712",
|
|
18
|
+
"payload": {
|
|
19
|
+
"version": 2,
|
|
20
|
+
"network": "eip155:84532",
|
|
21
|
+
"resourceUrl": "https://api.tokenizen.nl/api/paid/verify-ownership",
|
|
22
|
+
"payer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
23
|
+
"issuedAt": 1792187518,
|
|
24
|
+
"transaction": "",
|
|
25
|
+
"authorizationMethod": "eip3009",
|
|
26
|
+
"authorizationCommitment": "0xfecee93e1aec7d6858a218bf324f23e2a4eed07925d8d4542b9662c5026e3d22"
|
|
27
|
+
},
|
|
28
|
+
"signature": "0x02f5ba0d2b7928a2dbb1d8845b664a79d38eebcd0a2c4532ed3c6b922051bf8d47c8aafb0520a875a708e3ed480d84a537962ea90d66ce2ee3811f4229aa8b401b"
|
|
29
|
+
},
|
|
30
|
+
"authorizationSalt": "0x63072ea656fe922bf04c9cefc4275314d695fff01e5b57f6628c8df0252cc5dc",
|
|
31
|
+
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
32
|
+
"candidates": [
|
|
33
|
+
{
|
|
34
|
+
"authorizer": "0x87f344b6Ec48A936373d774D3De8649DEEB66Ee1",
|
|
35
|
+
"nonce": "0x1120412c78ea1b2e8bf9eacfc3264c71a23ecd9b1fa579eb16030693344d5dfe"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"context": {
|
|
40
|
+
"note": "Both nonces were really consumed by this payer; only one belongs to this receipt."
|
|
41
|
+
},
|
|
42
|
+
"expected": {
|
|
43
|
+
"valid": false,
|
|
44
|
+
"reason_code": "NOT_LINKED"
|
|
45
|
+
}
|
|
46
|
+
}
|