capacity-attest 0.5.0 → 0.7.0

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -167
  3. package/README.nl.md +204 -0
  4. package/dist/bench-helpers.d.ts +12 -0
  5. package/dist/bench-helpers.d.ts.map +1 -0
  6. package/dist/bench-helpers.js +51 -0
  7. package/dist/bench-helpers.js.map +1 -0
  8. package/dist/discovery.bench.d.ts +2 -0
  9. package/dist/discovery.bench.d.ts.map +1 -0
  10. package/dist/discovery.bench.js +30 -0
  11. package/dist/discovery.bench.js.map +1 -0
  12. package/dist/discovery.d.ts +2 -0
  13. package/dist/discovery.d.ts.map +1 -1
  14. package/dist/discovery.js +57 -2
  15. package/dist/discovery.js.map +1 -1
  16. package/dist/eas.bench.d.ts +2 -0
  17. package/dist/eas.bench.d.ts.map +1 -0
  18. package/dist/eas.bench.js +40 -0
  19. package/dist/eas.bench.js.map +1 -0
  20. package/dist/eas.d.ts +96 -10
  21. package/dist/eas.d.ts.map +1 -1
  22. package/dist/eas.js +158 -37
  23. package/dist/eas.js.map +1 -1
  24. package/dist/erc8004-reputation.d.ts +110 -0
  25. package/dist/erc8004-reputation.d.ts.map +1 -0
  26. package/dist/erc8004-reputation.js +203 -0
  27. package/dist/erc8004-reputation.js.map +1 -0
  28. package/dist/erc8004.d.ts +3 -1
  29. package/dist/erc8004.d.ts.map +1 -1
  30. package/dist/erc8004.js +20 -4
  31. package/dist/erc8004.js.map +1 -1
  32. package/dist/http-source.bench.d.ts +2 -0
  33. package/dist/http-source.bench.d.ts.map +1 -0
  34. package/dist/http-source.bench.js +65 -0
  35. package/dist/http-source.bench.js.map +1 -0
  36. package/dist/http-source.d.ts +28 -0
  37. package/dist/http-source.d.ts.map +1 -0
  38. package/dist/http-source.js +154 -0
  39. package/dist/http-source.js.map +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +6 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/ledger.d.ts.map +1 -1
  44. package/dist/ledger.js +93 -5
  45. package/dist/ledger.js.map +1 -1
  46. package/dist/schema.d.ts +1 -0
  47. package/dist/schema.d.ts.map +1 -1
  48. package/dist/schema.js +14 -2
  49. package/dist/schema.js.map +1 -1
  50. package/dist/tools.d.ts.map +1 -1
  51. package/dist/tools.js +6 -1
  52. package/dist/tools.js.map +1 -1
  53. package/package.json +63 -58
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdellah Ouadoudi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,167 +1,208 @@
1
- # capacity-attest
2
-
3
- MCP-server voor leverings-attestaties bij x402-capaciteitshandel tussen AI-agents.
4
-
5
- > Status: MVP, gepubliceerd op npm (`npm install capacity-attest`) en in het officiële MCP-register (`io.github.holistis/capacity-attest`).
6
-
7
- ## Waarom dit bestaat
8
-
9
- Wanneer een AI-agent via het [x402-protocol](https://www.x402.org/) betaalt voor capaciteit (GPU-uren, opslag, API/inference-credits, bandbreedte) bij een andere agent of dienst, is er na de betaling geen bewijs dat het beloofde ook echt geleverd is. De kopende agent weet het zelf (hij zag de output, of zag hem niet), maar die kennis gaat verloren zodra de sessie eindigt. De volgende agent die met dezelfde verkoper zaken wil doen, begint weer blind.
10
-
11
- `capacity-attest` lost dat specifieke gat op: na afwikkeling laat de **betalende** agent een cryptografisch ondertekende, feitelijke claim achter (`delivered: yes/no/partial` + een hash van het bewijsmateriaal). Andere agents kunnen die geschiedenis opvragen **voordat** ze zelf met die verkoper in zee gaan.
12
-
13
- Geen oordeel. Geen reputatiescore. Geen "vonnis", puur een ondertekende bon-plus-claim, net zoals een afleverbon bij een fysieke levering.
14
-
15
- ## Wat dit NIET is
16
-
17
- Dit is bewust en hardcoded **niet**:
18
-
19
- - **Geen reputatiescore of rating.** `get_delivery_history` retourneert de ruwe, chronologische lijst van claims, geen gemiddelde, geen percentage, geen "trust score". Het samenvatten tot één getal is impliciet een oordeel, en dat is expliciet afgewezen tijdens de besluitvorming voor dit project.
20
- - **Geen financieel product.** Geen rente, geen tijd-disconto op betalingen, geen yield op het ledger-saldo (er ís geen saldo, dit is geen escrow), geen lening, geen onderpand, geen invoice-financing/factoring. `assetType` is een gesloten enum van capaciteitssoorten (`gpu-hours`, `storage`, `api-credits`, `bandwidth`) en bevat bewust niets dat op een financieel instrument lijkt.
21
- - **Geen eigen token of munt.** Betalingen lopen via x402/USDC zoals gebruikelijk; dit project registreert alleen de *bon* van een afwikkeling die al ergens anders heeft plaatsgevonden.
22
- - **Geen krediet-verlening.** Een claim wordt pas gemaakt **na** een voltooide betaling. Dit project financiert niets, het documenteert een reeds afgeronde ijara (verhuur/dienst)-transactie.
23
- - **Geen eigen identity-, autoriteits- of geschillenlaag.** `externalRefs` (zie hieronder) is puur een citaat naar een systeem van een ander (ERC-8004, AP2, Legal Context Protocol, ...). Dit project resolvet, verifieert of beoordeelt die verwijzing zelf nooit. Zie [DECISIONS.md](./DECISIONS.md) D-007 t/m D-013 voor waarom dit bewust geen eigen protocol is geworden.
24
-
25
- Dit is een bewuste, formeel getoetste ontwerpkeuze, niet een toevallige scope-beperking. Zie de guardrails-sectie in het project-brief als je overweegt hier iets aan toe te voegen: bij twijfel of een veld/functie hiertegenaan schuurt, laat het weg.
26
-
27
- Bewust uitgestelde features (verankering, tussentijdse status, formele conformance-vectoren), inclusief de precieze voorwaarde waaronder we ze alsnog zouden bouwen: zie [DECISIONS.md](./DECISIONS.md).
28
-
29
- ## Hoe het werkt
30
-
31
- ### 1. `record_delivery`
32
-
33
- De betalende agent (de koper) roept dit aan **na** een x402-afwikkeling, zodra bekend is of het beloofde is aangekomen. De claim bevat:
34
-
35
- | Veld | Betekenis |
36
- | --- | --- |
37
- | `sellerAddress` | 0x-adres van de partij die betaald werd |
38
- | `buyerAddress` | 0x-adres van de betalende agent, moet overeenkomen met het adres dat uit `signature` wordt teruggerekend |
39
- | `assetType` | `gpu-hours` \| `storage` \| `api-credits` \| `bandwidth` |
40
- | `promisedSpec` | Wat er beloofd was: vrije tekst of een gestructureerd object |
41
- | `delivered` | `yes` \| `no` \| `partial` |
42
- | `evidenceHash` | sha256-hex van bewijsmateriaal (logs, response-payload, ...), het bewijs zelf wordt niet opgeslagen |
43
- | `settlementRef` | x402-payment-ref of on-chain tx-hash van de onderliggende betaling |
44
- | `timestamp` | ISO-8601 tijdstip |
45
- | `claimId` | content-addressed sha256-hash van alle velden hierboven, zie `computeClaimId()` in `src/schema.ts` |
46
- | `signature` | EIP-191 personal-sign handtekening van de koper over `claimId` |
47
- | `externalRefs` | *(optioneel, sinds 0.3.0)* ongeverifieerde verwijzingen naar andere agent-economie-infrastructuur: `sellerAgentRef`/`buyerAgentRef` (bv. een ERC-8004-agent-id of DID), `mandateRef`+`mandateIssuerDid` (een extern uitgegeven AP2/AAE-mandaat), `intentRef` (een extern AP2 IntentMandate), `disputeContext` (`protocol`+`termsHash`+optioneel `resolutionRef`, bv. een Legal Context Protocol-verwijzing). Zie [DECISIONS.md](./DECISIONS.md) D-007 t/m D-013 |
48
- | `priorClaimId` | *(optioneel, sinds 0.4.0)* de `claimId` van jouw vorige claim over dezelfde `sellerAddress`, zodat jouw claims over die verkoper een ketting vormen. Weggelaten bij je eerste claim over een verkoper. Zit in de ondertekende inhoud, dus een host kan het niet weghalen. Laat een lezer een host betrappen die een middelste claim verbergt. Zie [DECISIONS.md](./DECISIONS.md) D-006 |
49
-
50
- De server valideert eerst het schema, dan of `claimId` echt de hash van de inhoud is, en dan of `signature` echt terugrekent naar `buyerAddress`. Alleen dan wordt de claim toegevoegd aan de append-only ledger (`data/claims.jsonl`). Een ongeldige handtekening of een claim die al eerder is opgeslagen (zelfde `claimId`) wordt geweigerd.
51
-
52
- ### 2. `get_delivery_history`
53
-
54
- Gegeven een `sellerAddress`, retourneert dit alle bekende claims tegen die verkoper op déze installatie, chronologisch (oudst eerst). Puur feitelijk, geen samengevat getal. Een kopende agent roept dit aan **vóórdat** hij betaalt, om de ruwe leveringsgeschiedenis van een potentiële verkoper te zien en zelf te beoordelen.
55
-
56
- Het antwoord bevat naast `sellerAddress`, `count` en `claims` ook `scope` (altijd `"local-ledger"`) en `note`: een vaste, feitelijke tekst die uitlegt dat dit resultaat alleen de lokale ledger van déze installatie weerspiegelt. Een lege of korte geschiedenis betekent niet dat de verkoper een schone staat van dienst heeft, het kan ook betekenen dat er hier simpelweg nog geen claims zijn vastgelegd. Zie [DECISIONS.md](./DECISIONS.md) (D-005) voor de bredere architectuurvraag hierachter: hoe vindt een koper claims die op een ándere installatie zijn vastgelegd.
57
-
58
- Sinds 0.4.0 bevat het antwoord ook `completeness`: een analyse van de per-koper ketens (`priorClaimId`) in precies deze uitkomst. Als een getoonde claim terugverwijst naar een claim die NIET in de uitkomst zit, komt die in `possibleOmissions` te staan. Dat is een concreet, controleerbaar signaal dat de host mogelijk een middelste claim verbergt, in plaats van een vaag vermoeden.
59
-
60
- Let op, dit is het belangrijkste punt: dat `completeness`-veld wordt berekend door dezelfde server die de claims teruggeeft. Vertrouw je die server niet, vertrouw dan ook het veld niet, want een oneerlijke host kan er gewoon "alles compleet" in zetten. De echte zekerheid zit in de ondertekende `priorClaimId` in de claims zelf, die een host niet kan vervalsen of weghalen. Reken de controle dus zelf opnieuw uit over de teruggekregen claims:
61
-
62
- ```js
63
- // recompute-completeness.mjs
64
- import { verifyClaim } from "capacity-attest/dist/signing.js";
65
- import { analyzeCompleteness } from "capacity-attest/dist/completeness.js";
66
-
67
- // `claims` = de array uit het get_delivery_history-antwoord.
68
- const allSigned = claims.every((c) => verifyClaim(c).ok); // elke claim echt?
69
- const report = analyzeCompleteness(claims); // zelf herrekenen, niet het host-veld geloven
70
- console.log({ allSigned, chainConsistent: report.chainConsistent, possibleOmissions: report.possibleOmissions });
71
- ```
72
-
73
- Eerlijke grens: ook zelf-herrekenen betrapt geen verborgen laatste claim en geen verborgen hele koper, want daar valt geen schakel over te struikelen. En een losse terugverwijzing hoeft geen bedrog te zijn: de eerdere claim kan ook gewoon op een andere installatie zijn vastgelegd (het D-005-geval). Voor echte zekerheid blijven de externe getuigen nodig: je eigen bewaarde kopie hierboven, en de betaling op de keten via `settlementRef`. Zie [DECISIONS.md](./DECISIONS.md) D-006.
74
-
75
- Een openbaar, zelf-controleerbaar voorbeeld met een nagebootste verbergende host en expres-kapotte testgevallen staat in [docs/COMPLETENESS-FIXTURE.md](./docs/COMPLETENESS-FIXTURE.md). Draai het met `npm run fixture`; dezelfde controles draaien bij elke push als test. Zo kun je onze claim zelf natellen in plaats van ons op ons woord te geloven.
76
-
77
- ## Claims van andere installaties vinden (D-005)
78
-
79
- `get_delivery_history` is per definitie lokaal: koper B ziet niet wat koper A op een andere installatie vastlegde over dezelfde verkoper. Omdat elke claim zelf-verifieerbaar is, heeft vindbaarheid geen vertrouwde index nodig. `discoverDeliveryHistory(seller, sources)` (zie `src/discovery.ts`) leest een verkopers claims uit meerdere onafhankelijke, ONvertrouwde bronnen (je lokale ledger plus elk host-onafhankelijk substraat dat je wilt lezen), ontdubbelt, herverifieert elke claim, filtert andere verkopers eruit, en draait de completeness-check over het geheel. Een bron die nep injecteert wordt geweigerd; een bron die weglaat is het D-006-probleem, meegenomen maar niet magisch opgelost.
80
-
81
- De productie-onderlaag (EAS op Base, ERC-8004) is bewust nog niet live gekoppeld: dat kost gas en wacht op een echte integrator. De naad staat klaar. Een openbaar, draaibaar voorbeeld met twee nagebootste installaties staat in [docs/DISCOVERY-FIXTURE.md](./docs/DISCOVERY-FIXTURE.md), draai het met `npm run discovery-fixture`. Zie [DECISIONS.md](./DECISIONS.md) D-005.
82
-
83
- ### 3. `resolve_agent_identity` *(sinds 0.3.0)*
84
-
85
- Read-only opzoeking tegen een ERC-8004 Identity Registry: wie bezit `agentId` (`ownerOf`) en waar staat zijn registratiebestand (`tokenURI`). Alleen de standaard ERC-721-interface wordt aangeroepen, niets ERC-8004-specifieks. Vereist van de aanroeper zowel `agentRegistryRef` (`"eip155:<chainId>:<registryAddress>"`) als een `rpcUrl` voor die chain: dit project bundelt bewust geen eigen RPC-provider en geen canoniek registry-adres, want ERC-8004 heeft onafhankelijke deployments per chain en de EIP-tekst zelf noemt geen vast adres. Haalt bewust NOOIT op wat `tokenURI` aanwijst (dat blijft een pointer die de aanroeper zelf desgewenst opvraagt); dat zou een SSRF-vormig risico zijn op aanroeper-gecontroleerde on-chain data.
86
-
87
- Getest tegen een injecteerbare `ContractFactory` (`src/erc8004.test.ts`, geen netwerkafhankelijkheid) én live tegen de echte, gedeployde registry op Base mainnet (`examples/verify-erc8004-live.mjs`, `npm run build && node examples/verify-erc8004-live.mjs`). Zie [DECISIONS.md](./DECISIONS.md) D-007 voor de volledige achtergrond.
88
-
89
- ## Ondertekening
90
-
91
- De claim wordt ondertekend door de **koper** (de partij die betaalde en dus weet wat er wel/niet aankwam), niet door de verkoper. Dit is bewust eenvoudige EIP-191 `personal_sign` over `claimId` (via `ethers.Signer#signMessage`), geen EIP-712 typed data. Dat houdt het crypto-oppervlak van deze MVP klein en makkelijk te controleren. Een latere upgrade naar EIP-712 (zoals in `mcp-paywall/src/x402.mjs`) is additief mogelijk zonder bestaande claims ongeldig te maken.
92
-
93
- ## Een claim onafhankelijk verifiëren
94
-
95
- Elke claim in de ledger is met alleen het npm-package en de rauwe claim-bytes na te rekenen, zonder toegang tot dit project of een netwerkoproep naar ons. Geen account, geen hosted call.
96
-
97
- ```bash
98
- npm install capacity-attest
99
- ```
100
-
101
- ```js
102
- // verify.mjs, als ES module draaien (top-level await)
103
- import { verifyClaim } from "capacity-attest/dist/signing.js";
104
-
105
- const claim = JSON.parse(await (await fetch("<url naar een claim.jsonl-regel>")).text());
106
- console.log(verifyClaim(claim));
107
- // { ok: true } als claimId echt de hash van de inhoud is EN signature echt naar buyerAddress terugrekent
108
- ```
109
-
110
- Let op: importeer `capacity-attest/dist/signing.js` rechtstreeks, niet het package-root. De root (`dist/index.js`) start bij het importeren meteen de MCP-server over stdio, wat een los verificatie-script laat hangen.
111
-
112
- `verifyClaim()` controleert precies twee dingen: dat `claimId` de content-addressed hash van de claim-velden is, en dat `signature` (EIP-191) terugrekent naar `buyerAddress`. Het controleert niet of de onderliggende afwikkeling (`settlementRef`) echt on-chain klopt, dat is een losse, aparte check tegen de betreffende chain, en het controleert niet of `delivered` waar is of of `evidenceHash` een echt bewijsstuk dekt, dat blijft de eigen verklaring van de kopende agent.
113
-
114
- Een werkend, extern gereproduceerd voorbeeld van deze exacte stappen staat in [github.com/YE-YI7/asm-spec, PR #18](https://github.com/YE-YI7/asm-spec/pull/18): een onafhankelijk project dat dit tegen een echte, live geregistreerde claim heeft gedraaid.
115
-
116
- ## Je eigen ingediende claims delen, los van een host (D-006)
117
-
118
- `get_delivery_history` vertrouwt op de eerlijkheid van wie de MCP-server bedient: zie de `note` in dat tool-antwoord en [DECISIONS.md](./DECISIONS.md) (D-006). Elke getoonde claim is wel degelijk echt (ondertekening wordt sinds 2026-09-06 ook bij het lezen opnieuw gecontroleerd, niet alleen bij het schrijven), maar niets bewijst dat de host de VOLLEDIGE set laat zien die hij daadwerkelijk heeft.
119
-
120
- Als jij zelf de koper bent die een claim indiende, hoef je op die host niet te wachten: jij hebt die claim zelf al ondertekend, dus jij kan 'm rechtstreeks aan een wantrouwende tegenpartij laten zien, buiten elke host om.
121
-
122
- ```js
123
- // export-my-claims.mjs
124
- import { claimsForSeller } from "capacity-attest/dist/ledger.js";
125
-
126
- const myAddress = "0x..."; // jouw buyerAddress
127
- const seller = "0x..."; // de verkoper waar het over gaat
128
-
129
- const mine = (await claimsForSeller(seller)).filter(
130
- (c) => c.buyerAddress.toLowerCase() === myAddress.toLowerCase(),
131
- );
132
- console.log(JSON.stringify(mine, null, 2));
133
- ```
134
-
135
- Elke claim in die lijst is zelfstandig verifieerbaar met `verifyClaim()` (zie hierboven), zonder dat de ontvanger jouw installatie of enige host hoeft te vertrouwen. Dit lost geen vindbaarheid op (D-005: hoe vindt iemand anders jouw claim zonder dat jij 'm deelt) en geen volledigheid over ALLE kopers samen (D-006: dit bewijst alleen wat JIJ indiende, niet wat een host verder mogelijk verzwijgt van andere kopers), maar het geeft een concrete, kosteloze manier om één specifiek geschil te bewijzen zonder een host te hoeven vertrouwen.
136
-
137
- ## Lokaal draaien
138
-
139
- ```bash
140
- npm install
141
- npm run build # tsc -> dist/
142
- npm run typecheck # tsc --noEmit
143
- npm test # vitest run
144
- npm run demo # end-to-end lokale demo met TEST-sleutels, geen live infra
145
- npm start # start de MCP-server over stdio (bijv. voor Claude Desktop/Code als lokale MCP-server)
146
- ```
147
-
148
- De ledger-locatie is instelbaar via `CAPACITY_ATTEST_DATA_DIR` (default: `./data` in dit package). Tests en de demo gebruiken altijd een eigen, wegwerpbare tijdelijke map, nooit de echte `data/` map.
149
-
150
- ## Architectuur
151
-
152
- ```text
153
- src/
154
- schema.ts DeliveryClaim zod-schema + content-addressing (computeClaimId, canonicalize)
155
- signing.ts sign/verify van een claim (ethers, EIP-191 personal-sign)
156
- ledger.ts append-only JSONL-opslag (data/claims.jsonl), nooit muteerbaar
157
- tools.ts de daadwerkelijke logica achter beide MCP-tools, transport-onafhankelijk
158
- config.ts waar de ledger-map leeft, lazy zodat tests 'm kunnen overriden
159
- index.ts MCP-server wiring (registreert record_delivery + get_delivery_history)
160
- examples/demo.ts end-to-end lokaal voorbeeld met TEST-sleutels
161
- ```
162
-
163
- `tools.ts` bevat de eigenlijke business-logica; `index.ts` vertaalt dat alleen naar MCP tool-calls. Zo kunnen tests en de demo dezelfde logica direct aanroepen zonder een stdio-transport op te tuigen.
164
-
165
- ## Relatie tot x402
166
-
167
- Dit project verifieert of settelt zelf géén x402-betalingen, dat gebeurt al bij de betaalstap zelf (zie bijvoorbeeld `mcp-paywall/src/x402.mjs` in dit ecosysteem voor een volledige EIP-3009-verify/settle-implementatie). `settlementRef` verwijst simpelweg naar die reeds-voltooide afwikkeling. Dat betekent ook dat de MVP-koppeling met een echte x402-facilitator eenvoudig kan blijven: `settlementRef` is vrije tekst, met als aanname dat de koper 'm eerlijk invult. Een latere versie kan dat veld optioneel verifiëren tegen een echte facilitator (TODO, niet in deze MVP).
1
+ # capacity-attest
2
+
3
+ *[Nederlandse versie / Dutch version: README.nl.md](README.nl.md)*
4
+
5
+ MCP server for delivery attestations in x402 capacity trading between AI agents.
6
+
7
+ > Status: MVP, published on npm (`npm install capacity-attest`) and in the official MCP registry (`io.github.holistis/capacity-attest`).
8
+
9
+ ## Independently checked, not just claimed
10
+
11
+ Every claim about this project below is clickable and independently verifiable, not something you have to take our word for.
12
+
13
+ | What | By whom | Status |
14
+ |---|---|---|
15
+ | Uses `capacity-attest@0.2.0` as a real dependency, verifies claim digest/claimId/signature through our own code | [YE-YI7/asm-spec#18](https://github.com/YE-YI7/asm-spec/pull/18) | Merged |
16
+ | BSV rail adapter on the same content-addressed claim format | [YE-YI7/asm-spec#19](https://github.com/YE-YI7/asm-spec/pull/19) (author EmbryoSpace) | Merged |
17
+ | Our boundary claims ("discoverability ≠ completeness") independently verified on-chain by a third party, nothing taken on faith | [x402-foundation/x402#3379](https://github.com/x402-foundation/x402/issues/3379) | Public, ongoing |
18
+ | Live delivery claims as on-chain attestations on Base mainnet, decodable by anyone | [delivered=yes](https://base.easscan.org/attestation/view/0x81a55d54452b2cf8bdda7918f63a27bf9ff79e5025b485f7316aae6259288ccc) · [delivered=no](https://base.easscan.org/attestation/view/0xe736b005cbcb54f8f196ac64ef09d75d939c8a18c0d5d9670b5c5025c07398c4) | Live |
19
+ | The same, live on a second and third chain (WP2), proving the EAS source works across deployments, not only Base | [Optimism](https://optimism.easscan.org/attestation/view/0x46148283cb005aa43387fb62b2e1ccd0b001ff82dc9310ea885237fd8dea8832) · [Ethereum mainnet](https://easscan.org/attestation/view/0x40da382231eeb6186b7daf231a430488627769feb64039ae3ac671a14f7a8137) | Live |
20
+ | A real `giveFeedback()` call on the ERC-8004 Reputation Registry, Base mainnet | [tx 0x2217...efc0](https://basescan.org/tx/0x221797800d5941dff62e87022083e7c6dfba3e07b35c84e56b10fdca8967efc0) | Live |
21
+ | Proposed as a buyer-side addition to someone else's agent spec | [omworldprotocol/om-world#18](https://github.com/omworldprotocol/om-world/pull/18) | In review, not yet merged |
22
+ | An independently rebuilt, stdlib-only Python verifier (its own secp256k1/EIP-191/canonical-JSON implementation, no shared code), 7/7 match against our own test vectors including both negative controls (forged signature, tampered claim), now a permanent regression test in a separate, independently published project | [x402-foundation/x402#2887 (comment)](https://github.com/x402-foundation/x402/issues/2887#issuecomment-5687398497), author goun7, integrated into Tamga (`pip install tamga-protocol`) | Permanently integrated into someone else's project |
23
+
24
+ **Kept honestly separate from the table above, because this is not third-party review:** before 0.6.0 (the first version that actually writes to the blockchain), we ran our own adversarial security review. Ten findings, all fixed with a dedicated regression test, not an independent audit by an outside party. Full, checkable report: [docs/SECURITY-REVIEW-2026-09-11.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/docs/SECURITY-REVIEW-2026-09-11.md).
25
+
26
+ ## Why this exists
27
+
28
+ When an AI agent pays via the [x402 protocol](https://www.x402.org/) for capacity (GPU hours, storage, API/inference credits, bandwidth) from another agent or service, there is no proof after payment that what was promised was actually delivered. The buying agent knows it firsthand (it saw the output, or didn't), but that knowledge is lost the moment the session ends. The next agent looking to do business with the same seller starts blind again.
29
+
30
+ `capacity-attest` closes that specific gap: after settlement, the **paying** agent leaves behind a cryptographically signed, factual claim (`delivered: yes/no/partial` + a hash of the evidence). Other agents can pull that history **before** they do business with that same seller.
31
+
32
+ No judgment. No reputation score. No "verdict" — just a signed receipt-plus-claim, the same way you'd get a delivery slip for a physical shipment.
33
+
34
+ ## What this deliberately is NOT
35
+
36
+ This is deliberately and permanently **not**:
37
+
38
+ - **Not a reputation score or rating.** `get_delivery_history` returns the raw, chronological list of claims, no average, no percentage, no "trust score". Summarizing it into a single number is implicitly a judgment, and that was explicitly rejected during this project's design phase.
39
+ - **Not a financial product.** No interest, no time-discounting on payments, no yield on a ledger balance (there is no balance, this is not an escrow), no lending, no collateral, no invoice financing/factoring. `assetType` is a closed enum of capacity kinds (`gpu-hours`, `storage`, `api-credits`, `bandwidth`) and deliberately contains nothing resembling a financial instrument.
40
+ - **Not its own token or coin.** Payments run through x402/USDC as usual; this project only records the *receipt* of a settlement that already happened elsewhere.
41
+ - **Not credit extension.** A claim is only created **after** a completed payment. This project finances nothing; it documents an already-completed ijara (rental/service) transaction.
42
+ - **Not its own identity, authority, or dispute-resolution layer.** `externalRefs` (see below) is purely a citation to someone else's system (ERC-8004, AP2, Legal Context Protocol, ...). This project never resolves, verifies, or judges that reference itself. See [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) D-007 through D-013 for why this deliberately did not become its own protocol.
43
+
44
+ This is a deliberate, formally reviewed design choice, not an incidental scope limit. See the guardrails section in the project brief if you're considering adding something here: when in doubt whether a field/function brushes against this line, leave it out.
45
+
46
+ Deliberately deferred features (anchoring, interim status, formal conformance vectors), including the exact condition under which we'd build them anyway: see [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md).
47
+
48
+ Further reading: [nine ways to fake a delivery claim, and why none of them fully worked](https://tokenizen.nl/en/notes/nine-ways-to-fake-a-delivery-claim), the public write-up of D-014/D-018 above.
49
+
50
+ ## How it works
51
+
52
+ ### 1. `record_delivery`
53
+
54
+ The paying agent (the buyer) calls this **after** an x402 settlement, once it's known whether what was promised arrived. The claim contains:
55
+
56
+ | Field | Meaning |
57
+ | --- | --- |
58
+ | `sellerAddress` | 0x address of the party that was paid |
59
+ | `buyerAddress` | 0x address of the paying agent, must match the address recovered from `signature` |
60
+ | `assetType` | `gpu-hours` \| `storage` \| `api-credits` \| `bandwidth` |
61
+ | `promisedSpec` | What was promised: free text or a structured object |
62
+ | `delivered` | `yes` \| `no` \| `partial` |
63
+ | `evidenceHash` | sha256 hex of the supporting evidence (logs, response payload, ...); the evidence itself is not stored |
64
+ | `settlementRef` | x402 payment ref or on-chain tx hash of the underlying payment |
65
+ | `timestamp` | ISO-8601 timestamp |
66
+ | `claimId` | content-addressed sha256 hash of all the fields above, see `computeClaimId()` in `src/schema.ts` |
67
+ | `signature` | EIP-191 personal-sign signature by the buyer over `claimId` |
68
+ | `externalRefs` | *(optional, since 0.3.0)* unverified references to other agent-economy infrastructure: `sellerAgentRef`/`buyerAgentRef` (e.g. an ERC-8004 agent id or DID), `mandateRef`+`mandateIssuerDid` (an externally issued AP2/AAE mandate), `intentRef` (an external AP2 IntentMandate), `disputeContext` (`protocol`+`termsHash`+optional `resolutionRef`, e.g. a Legal Context Protocol reference). See [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) D-007 through D-013 |
69
+ | `priorClaimId` | *(optional, since 0.4.0)* the `claimId` of your previous claim about the same `sellerAddress`, so your claims about that seller form a chain. Omitted on your first claim about a seller. Part of the signed content, so a host can't strip it. Lets a reader catch a host hiding a middle claim. See [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) D-006 |
70
+
71
+ The server first validates the schema, then whether `claimId` really is the hash of the content, then whether `signature` really recovers to `buyerAddress`. Only then is the claim appended to the append-only ledger (`data/claims.jsonl`). An invalid signature or a claim that's already stored (same `claimId`) is rejected.
72
+
73
+ ### 2. `get_delivery_history`
74
+
75
+ Given a `sellerAddress`, this returns all known claims against that seller on **this** installation, chronologically (oldest first). Purely factual, no summarized number. A buying agent calls this **before** paying, to see the raw delivery history of a prospective seller and judge it themselves.
76
+
77
+ The response includes `sellerAddress`, `count` and `claims`, plus `scope` (always `"local-ledger"`) and `note`: a fixed, factual text explaining that this result only reflects the local ledger of **this** installation. An empty or short history doesn't mean the seller has a clean record — it can also mean no claims have been recorded here yet.
78
+
79
+ Since 0.4.0 the response also includes `completeness`: an analysis of the per-buyer chains (`priorClaimId`) within exactly this result set. If a claim shown here refers back to a claim that is NOT in the result set, that shows up in `possibleOmissions`. That's a concrete, checkable signal that the host might be hiding a middle claim, rather than a vague suspicion.
80
+
81
+ Note, and this is the most important point: that `completeness` field is computed by the same server that returns the claims. If you don't trust that server, don't trust that field either — a dishonest host can just set "everything's complete" regardless. The real guarantee lives in the signed `priorClaimId` inside the claims themselves, which a host cannot forge or remove. So recompute the check yourself over the claims you got back:
82
+
83
+ ```js
84
+ // recompute-completeness.mjs
85
+ import { verifyClaim } from "capacity-attest/dist/signing.js";
86
+ import { analyzeCompleteness } from "capacity-attest/dist/completeness.js";
87
+
88
+ // `claims` = the array from the get_delivery_history response.
89
+ const allSigned = claims.every((c) => verifyClaim(c).ok); // is every claim genuine?
90
+ const report = analyzeCompleteness(claims); // recompute yourself, don't trust the host field
91
+ console.log({ allSigned, chainConsistent: report.chainConsistent, possibleOmissions: report.possibleOmissions });
92
+ ```
93
+
94
+ Honest limit: even recomputing yourself won't catch a hidden *last* claim, or a whole hidden buyer, because there's no link to trip over in either case. And a dangling back-reference isn't necessarily foul play either — the earlier claim might simply have been recorded on a different installation (the D-005 case). For real certainty you still need external witnesses: your own retained copy from above, and the on-chain payment via `settlementRef`. See [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) D-006.
95
+
96
+ A public, self-checkable example with a simulated hiding host and deliberately broken test cases is in [docs/COMPLETENESS-FIXTURE.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/docs/COMPLETENESS-FIXTURE.md). Run it with `npm run fixture`; the same checks run on every push as a test. So you can verify our claim yourself instead of taking our word for it.
97
+
98
+ ## Finding claims from other installations (D-005)
99
+
100
+ `get_delivery_history` is local by definition: buyer B doesn't see what buyer A recorded about the same seller on a different installation. Because every claim is self-verifiable, discoverability doesn't need a trusted index. `discoverDeliveryHistory(seller, sources)` (see `src/discovery.ts`) reads a seller's claims from multiple independent, UNTRUSTED sources (your local ledger plus any host-independent substrate you want to read), deduplicates, re-verifies every claim, filters out other sellers, and runs the completeness check over the combined set. A source that injects fakes gets rejected; a source that omits things is the D-006 problem — accounted for, not magically solved.
101
+
102
+ The production substrate is live on all three chains this project targets: EAS attestations exist on Base mainnet, Optimism mainnet, and Ethereum mainnet, each independently verifiable via a public block explorer link, not just described. A public, runnable example with two simulated installations is in [docs/DISCOVERY-FIXTURE.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/docs/DISCOVERY-FIXTURE.md), run it with `npm run discovery-fixture`. See [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) D-005.
103
+
104
+ A second, non-EAS production-grade `ClaimSource` also ships: `httpClaimSource` (`src/http-source.ts`), a plain HTTP endpoint following the convention `GET <url>?seller=<address>` -> a JSON array of claims, one concrete shape of "another installation's public endpoint" from `discoverDeliveryHistory`'s own header comment. It exists to prove the aggregation layer against a genuinely different source type, not just a second EAS deployment; like every source it is untrusted, so a malformed or hostile response never crashes discovery, it just shows up as a per-source error. `examples/reputation-lookup.mjs` is a runnable CLI combining the local ledger with both production sources (EAS and this one) in a single real lookup: `npm run build && node examples/reputation-lookup.mjs <sellerAddress>` (optionally set `RPC_URL` and/or `HTTP_SOURCE_URL` first; it needs no funded key, since it only reads).
105
+
106
+ [docs/INTEGRATION-GUIDE.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/docs/INTEGRATION-GUIDE.md) walks through all three source types with real code, plus two independent, real integrations built by other projects (a Base-linkage fixture and a BSV rail, neither built by us) as concrete worked examples of extending this claim format to a new chain.
107
+
108
+ ### 3. `resolve_agent_identity` *(since 0.3.0)*
109
+
110
+ Read-only lookup against an ERC-8004 Identity Registry: who owns `agentId` (`ownerOf`) and where is its registration file (`tokenURI`). Only the standard ERC-721 interface is called, nothing ERC-8004-specific. Requires the caller to supply both `agentRegistryRef` (`"eip155:<chainId>:<registryAddress>"`) and an `rpcUrl` for that chain: this project deliberately bundles no own RPC provider and no canonical registry address, since ERC-8004 has independent deployments per chain and the EIP text itself names no fixed address. Deliberately NEVER fetches what `tokenURI` points to (that stays a pointer the caller can retrieve themselves if they want); doing so would be an SSRF-shaped risk on caller-controlled on-chain data.
111
+
112
+ Tested against an injectable `ContractFactory` (`src/erc8004.test.ts`, no network dependency) and live against the real, deployed registry on Base mainnet (`examples/verify-erc8004-live.mjs`, `npm run build && node examples/verify-erc8004-live.mjs`). See [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) D-007 for the full background.
113
+
114
+ ### 4. `publishReputationFeedback` *(since 0.6.0, library function, not an MCP tool)*
115
+
116
+ **Note (adversarial review 2026-09-11): at the time of writing, npm still has version 0.5.0 published, without this function.** Anyone reading via GitHub and immediately running `npm install capacity-attest` as described further below will not yet get `publishReputationFeedback` — check `npm view capacity-attest version` for the actually published version before importing this. Everything below describes the code as it stands on the `main` branch.
117
+
118
+ Publishes the `delivered` fact of an already-signed claim to an ERC-8004 Reputation Registry's `giveFeedback()`, the same place roughly 500k registered agents can already look for reputation signals, instead of only this installation's own ledger or EAS. The contract requires a numeric `value`+`valueDecimals` field; this package deliberately does not invent its own rating scale for that. `value` is a literal, mechanical mirror of `delivered` (yes=1.0, partial=0.5, no=0.0), never a new judgment, and capacity-attest never reads or displays that number back anywhere itself. Re-verifies the claim's signature before writing anything on-chain.
119
+
120
+ Requires the caller to supply `reputationRegistryRef` (`"eip155:<chainId>:<registryAddress>"`, the Reputation Registry, not the Identity Registry), `agentRegistryRef` (same chain, but the Identity Registry) and an `rpcUrl`, the same caller-supplies-everything stance as `resolve_agent_identity`. `agentId` (the seller's ERC-8004 agent) must already be a validly registered Identity Registry agent; the contract itself refuses feedback from the agent's own owner ("Self-feedback not allowed"). Since the 2026-09-11 adversarial review, `agentId`'s registered owner (via `agentRegistryRef`) is also always checked against `claim.sellerAddress` before anything is written on-chain — without that check, a caller could attach a genuine, validly signed claim to an arbitrary other agentId.
121
+
122
+ **Deliberately NOT an MCP tool**, for the same reason as EAS's `publishClaim`: this is a write action that requires a real, funded signer and gas, and this server deliberately bundles or stores no private key of its own. Available as a direct import (`src/erc8004-reputation.ts`) for anyone managing their own signer.
123
+
124
+ Tested against an injectable `ReputationContractFactory` (`src/erc8004-reputation.test.ts`, 19 tests, no network dependency, including an explicit test that `value` depends solely on `delivered`, and three tests for the agentId ownership check) and live against the real, deployed registry on Base mainnet (`examples/erc8004-reputation-live-demo.ts`, `npm run erc8004-reputation-demo`): confirmed on 2026-09-10 via our own, disposable test agent (agentId 85888) and a real `giveFeedback()` call, [tx 0x221797800d5941dff62e87022083e7c6dfba3e07b35c84e56b10fdca8967efc0](https://basescan.org/tx/0x221797800d5941dff62e87022083e7c6dfba3e07b35c84e56b10fdca8967efc0), independently double-checked via a separate `eth_getTransactionReceipt` call. See [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) D-016 for the full background, including why this got built today despite D-005's own trigger criterion.
125
+
126
+ ## Signing
127
+
128
+ The claim is signed by the **buyer** (the party that paid and therefore knows what did or didn't arrive), not by the seller. This is deliberately plain EIP-191 `personal_sign` over `claimId` (via `ethers.Signer#signMessage`), not EIP-712 typed data. That keeps this MVP's crypto surface small and easy to audit. A later upgrade to EIP-712 (like in `mcp-paywall/src/x402.mjs`) is possible additively, without invalidating existing claims.
129
+
130
+ ## Independently verifying a claim
131
+
132
+ Every claim in the ledger can be re-checked with only the npm package and the raw claim bytes, no access to this project or a network call to us needed. No account, no hosted call.
133
+
134
+ ```bash
135
+ npm install capacity-attest
136
+ ```
137
+
138
+ ```js
139
+ // verify.mjs, run as an ES module (top-level await)
140
+ import { verifyClaim } from "capacity-attest/dist/signing.js";
141
+
142
+ const claim = JSON.parse(await (await fetch("<url to a claim.jsonl line>")).text());
143
+ console.log(verifyClaim(claim));
144
+ // { ok: true } if claimId really is the hash of the content AND signature really recovers to buyerAddress
145
+ ```
146
+
147
+ Note: import `capacity-attest/dist/signing.js` directly, not the package root. The root (`dist/index.js`) starts the MCP server over stdio the moment it's imported, which will hang a standalone verification script.
148
+
149
+ `verifyClaim()` checks exactly two things: that `claimId` is the content-addressed hash of the claim fields, and that `signature` (EIP-191) recovers to `buyerAddress`. It does not check whether the underlying settlement (`settlementRef`) really checks out on-chain — that's a separate check against the relevant chain — and it does not check whether `delivered` is true or whether `evidenceHash` covers real evidence; that remains the paying agent's own statement.
150
+
151
+ A working, externally reproduced example of these exact steps is in [github.com/YE-YI7/asm-spec, PR #18](https://github.com/YE-YI7/asm-spec/pull/18): an independent project that ran this against a real, live registered claim.
152
+
153
+ ## Sharing your own submitted claims, independent of a host (D-006)
154
+
155
+ `get_delivery_history` relies on the honesty of whoever operates the MCP server: see the `note` in that tool's response and [DECISIONS.md](https://github.com/holistis/tokenizen/blob/main/packages/capacity-attest/DECISIONS.md) (D-006). Every claim shown is genuinely real (the signature has also been re-checked on read since 2026-09-06, not only on write), but nothing proves the host is showing the FULL set it actually has.
156
+
157
+ If you're the buyer who submitted a claim yourself, you don't need to wait on that host: you already signed that claim yourself, so you can show it directly to a skeptical counterparty, bypassing any host.
158
+
159
+ ```js
160
+ // export-my-claims.mjs
161
+ import { claimsForSeller } from "capacity-attest/dist/ledger.js";
162
+
163
+ const myAddress = "0x..."; // your buyerAddress
164
+ const seller = "0x..."; // the seller in question
165
+
166
+ const mine = (await claimsForSeller(seller)).filter(
167
+ (c) => c.buyerAddress.toLowerCase() === myAddress.toLowerCase(),
168
+ );
169
+ console.log(JSON.stringify(mine, null, 2));
170
+ ```
171
+
172
+ Every claim in that list is independently verifiable with `verifyClaim()` (see above), without the recipient having to trust your installation or any host. This doesn't solve discoverability (D-005: how does someone else find your claim if you don't share it) or completeness across ALL buyers together (D-006: this only proves what YOU submitted, not what a host might otherwise be withholding from other buyers), but it gives you a concrete, free way to prove one specific dispute without needing to trust a host.
173
+
174
+ ## Running locally
175
+
176
+ ```bash
177
+ npm install
178
+ npm run build # tsc -> dist/
179
+ npm run typecheck # tsc --noEmit
180
+ npm test # vitest run
181
+ npm run demo # end-to-end local demo with TEST keys, no live infrastructure
182
+ npm start # start the MCP server over stdio (e.g. for Claude Desktop/Code as a local MCP server)
183
+ ```
184
+
185
+ The ledger location is configurable via `CAPACITY_ATTEST_DATA_DIR` (default: `./data` in this package). Tests and the demo always use their own, disposable temp directory, never the real `data/` folder.
186
+
187
+ ## Architecture
188
+
189
+ ```text
190
+ src/
191
+ schema.ts DeliveryClaim zod schema + content-addressing (computeClaimId, canonicalize)
192
+ signing.ts sign/verify a claim (ethers, EIP-191 personal-sign)
193
+ ledger.ts append-only JSONL storage (data/claims.jsonl), never mutated
194
+ tools.ts the actual logic behind both MCP tools, transport-agnostic
195
+ config.ts where the ledger directory lives, lazy so tests can override it
196
+ index.ts MCP server wiring (registers record_delivery + get_delivery_history)
197
+ examples/demo.ts end-to-end local example with TEST keys
198
+ ```
199
+
200
+ `tools.ts` holds the actual business logic; `index.ts` only translates that into MCP tool calls. That way tests and the demo can call the same logic directly without spinning up a stdio transport.
201
+
202
+ ## Relation to x402
203
+
204
+ This project itself verifies or settles no x402 payments — that already happens at the payment step (see e.g. `mcp-paywall/src/x402.mjs` in this ecosystem for a full EIP-3009 verify/settle implementation). `settlementRef` simply points at that already-completed settlement. That also means the MVP integration with a real x402 facilitator can stay simple: `settlementRef` is free text, on the assumption that the buyer fills it in honestly. A later version could optionally verify that field against a real facilitator (TODO, not in this MVP).
205
+
206
+ ## Relation to AWS Bedrock AgentCore Payments
207
+
208
+ No overlap, no competition: different step in the chain. Bedrock AgentCore Payments (Amazon, since 2026) handles the payment step itself, up to and including the moment "the merchant verifies the payment proof... [and] returns the requested content" ([official AWS documentation](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-how-it-works.html)). That proof is proof of **payment**, not of **delivery**: nowhere is it recorded whether the agent actually received what was promised after that step. That's exactly where `capacity-attest` picks up. Same as with x402 above: this project settles no payments and doesn't compete with the payment rail — it records what did or didn't actually arrive after payment, regardless of which rail (x402 or otherwise) handled that payment.