capacity-attest 0.1.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.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # capacity-attest
2
+
3
+ MCP-server voor leverings-attestaties bij x402-capaciteitshandel tussen AI-agents.
4
+
5
+ > Status: MVP, **nog niet gepubliceerd** (`"private": true` in `package.json`). Structuur is publicatie-klaar (zie `server.json`), maar er is bewust nog geen `npm publish` of `git push` naar een remote gedaan.
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
+
24
+ Dit is een sharia-getoetste (halal) 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.
25
+
26
+ ## Hoe het werkt
27
+
28
+ ### 1. `record_delivery`
29
+
30
+ De betalende agent (de koper) roept dit aan **na** een x402-afwikkeling, zodra bekend is of het beloofde is aangekomen. De claim bevat:
31
+
32
+ | Veld | Betekenis |
33
+ | --- | --- |
34
+ | `sellerAddress` | 0x-adres van de partij die betaald werd |
35
+ | `buyerAddress` | 0x-adres van de betalende agent — moet overeenkomen met het adres dat uit `signature` wordt teruggerekend |
36
+ | `assetType` | `gpu-hours` \| `storage` \| `api-credits` \| `bandwidth` |
37
+ | `promisedSpec` | Wat er beloofd was — vrije tekst of een gestructureerd object |
38
+ | `delivered` | `yes` \| `no` \| `partial` |
39
+ | `evidenceHash` | sha256-hex van bewijsmateriaal (logs, response-payload, ...) — het bewijs zelf wordt niet opgeslagen |
40
+ | `settlementRef` | x402-payment-ref of on-chain tx-hash van de onderliggende betaling |
41
+ | `timestamp` | ISO-8601 tijdstip |
42
+ | `claimId` | content-addressed sha256-hash van alle velden hierboven — zie `computeClaimId()` in `src/schema.ts` |
43
+ | `signature` | EIP-191 personal-sign handtekening van de koper over `claimId` |
44
+
45
+ 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.
46
+
47
+ ### 2. `get_delivery_history`
48
+
49
+ Gegeven een `sellerAddress`, retourneert dit alle bekende claims tegen die verkoper, 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.
50
+
51
+ ## Ondertekening
52
+
53
+ 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.
54
+
55
+ ## Lokaal draaien
56
+
57
+ ```bash
58
+ npm install
59
+ npm run build # tsc -> dist/
60
+ npm run typecheck # tsc --noEmit
61
+ npm test # vitest run
62
+ npm run demo # end-to-end lokale demo met TEST-sleutels, geen live infra
63
+ npm start # start de MCP-server over stdio (bijv. voor Claude Desktop/Code als lokale MCP-server)
64
+ ```
65
+
66
+ 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.
67
+
68
+ ## Architectuur
69
+
70
+ ```
71
+ src/
72
+ schema.ts DeliveryClaim zod-schema + content-addressing (computeClaimId, canonicalize)
73
+ signing.ts sign/verify van een claim (ethers, EIP-191 personal-sign)
74
+ ledger.ts append-only JSONL-opslag (data/claims.jsonl), nooit muteerbaar
75
+ tools.ts de daadwerkelijke logica achter beide MCP-tools, transport-onafhankelijk
76
+ config.ts waar de ledger-map leeft, lazy zodat tests 'm kunnen overriden
77
+ index.ts MCP-server wiring (registreert record_delivery + get_delivery_history)
78
+ examples/demo.ts end-to-end lokaal voorbeeld met TEST-sleutels
79
+ ```
80
+
81
+ `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.
82
+
83
+ ## Relatie tot x402
84
+
85
+ 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).
@@ -0,0 +1,6 @@
1
+ export declare const ROOT: string;
2
+ /** Directory the claims ledger (claims.jsonl) is read from / written to. */
3
+ export declare function dataDir(): string;
4
+ /** Ensure the data directory exists and return its path. */
5
+ export declare function ensureDataDir(): string;
6
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,IAAI,QAAwB,CAAC;AAE1C,4EAA4E;AAC5E,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,IAAI,MAAM,CAItC"}
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ // config.ts — where the append-only claims ledger lives on disk.
2
+ //
3
+ // The directory is resolved lazily (a function, not a module-load-time
4
+ // constant) specifically so tests and examples/demo.ts can point at an
5
+ // isolated, disposable directory by setting CAPACITY_ATTEST_DATA_DIR right
6
+ // before calling a ledger function — no import-order gymnastics required.
7
+ import { existsSync, mkdirSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+ import { dirname, join } from "node:path";
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ export const ROOT = join(__dirname, "..");
12
+ /** Directory the claims ledger (claims.jsonl) is read from / written to. */
13
+ export function dataDir() {
14
+ return process.env["CAPACITY_ATTEST_DATA_DIR"] || join(ROOT, "data");
15
+ }
16
+ /** Ensure the data directory exists and return its path. */
17
+ export function ensureDataDir() {
18
+ const dir = dataDir();
19
+ if (!existsSync(dir))
20
+ mkdirSync(dir, { recursive: true });
21
+ return dir;
22
+ }
23
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,uEAAuE;AACvE,uEAAuE;AACvE,2EAA2E;AAC3E,0EAA0E;AAE1E,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;AAE1C,4EAA4E;AAC5E,MAAM,UAAU,OAAO;IACrB,OAAO,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACvE,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,aAAa;IAC3B,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { ASSET_TYPES, DELIVERED_VALUES } from "./schema.js";
3
+ export { ASSET_TYPES, DELIVERED_VALUES };
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AASA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAA2C,MAAM,aAAa,CAAC;AAwDrG,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // index.ts — MCP server wiring. All actual logic lives in tools.ts; this
3
+ // file only translates MCP tool calls into calls on that logic and formats
4
+ // the result as MCP content blocks (same pattern as
5
+ // al-yad-mcp-server/packages/mcp-server/src/index.ts).
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { ASSET_TYPES, DELIVERED_VALUES, ClaimContentSchema, DeliveryClaimSchema } from "./schema.js";
9
+ import { recordDelivery, getDeliveryHistory } from "./tools.js";
10
+ const server = new McpServer({
11
+ name: "capacity-attest",
12
+ version: "0.1.0",
13
+ });
14
+ function textResult(value) {
15
+ const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
16
+ return { content: [{ type: "text", text }] };
17
+ }
18
+ function errorResult(message) {
19
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
20
+ }
21
+ server.registerTool("record_delivery", {
22
+ title: "Record a delivery claim",
23
+ description: "Record a signed delivery claim after an x402 capacity settlement (gpu-hours, storage, api-credits, or bandwidth). " +
24
+ "The buyer agent calls this AFTER paying, once it knows whether what was promised actually arrived. " +
25
+ "The claim's schema and signature (must recover to buyerAddress) are validated before it is written to the " +
26
+ "append-only ledger. This is a factual receipt, not a reputation score — see get_delivery_history for how " +
27
+ "other agents read it back.",
28
+ inputSchema: DeliveryClaimSchema.shape,
29
+ }, async (args) => {
30
+ const result = recordDelivery(args);
31
+ if (!result.ok)
32
+ return errorResult(result.reason);
33
+ return textResult({ ok: true, claimId: result.claimId });
34
+ });
35
+ server.registerTool("get_delivery_history", {
36
+ title: "Get a seller's delivery history",
37
+ description: "Return every known, signature-verified delivery claim recorded against a given sellerAddress, oldest first. " +
38
+ "Purely factual — no aggregate score, rating, or reputation judgment is computed. A buying agent can call this " +
39
+ "BEFORE paying a seller to see that seller's raw delivery history for gpu-hours, storage, api-credits, and " +
40
+ "bandwidth claims.",
41
+ inputSchema: {
42
+ sellerAddress: ClaimContentSchema.shape.sellerAddress,
43
+ },
44
+ }, async ({ sellerAddress }) => {
45
+ return textResult(getDeliveryHistory(sellerAddress));
46
+ });
47
+ // Re-exported so callers embedding this package can reference the same enums
48
+ // / schema the tools validate against without duplicating them.
49
+ export { ASSET_TYPES, DELIVERED_VALUES };
50
+ async function main() {
51
+ const transport = new StdioServerTransport();
52
+ await server.connect(transport);
53
+ }
54
+ main().catch((e) => {
55
+ console.error("capacity-attest MCP server failed to start:", e);
56
+ process.exit(1);
57
+ });
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,yEAAyE;AACzE,2EAA2E;AAC3E,oDAAoD;AACpD,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,iBAAiB;IACvB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,KAAc;IAChC,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAChF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,OAAO,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACnF,CAAC;AAED,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;IACE,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,oHAAoH;QACpH,qGAAqG;QACrG,4GAA4G;QAC5G,2GAA2G;QAC3G,4BAA4B;IAC9B,WAAW,EAAE,mBAAmB,CAAC,KAAK;CACvC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClD,OAAO,UAAU,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;AAC3D,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,YAAY,CACjB,sBAAsB,EACtB;IACE,KAAK,EAAE,iCAAiC;IACxC,WAAW,EACT,8GAA8G;QAC9G,gHAAgH;QAChH,4GAA4G;QAC5G,mBAAmB;IACrB,WAAW,EAAE;QACX,aAAa,EAAE,kBAAkB,CAAC,KAAK,CAAC,aAAa;KACtD;CACF,EACD,KAAK,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE;IAC1B,OAAO,UAAU,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC,CAAC;AACvD,CAAC,CACF,CAAC;AAEF,6EAA6E;AAC7E,gEAAgE;AAChE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC;AAEzC,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,6CAA6C,EAAE,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type DeliveryClaim } from "./schema.js";
2
+ /**
3
+ * Append one already-verified claim to the ledger. Throws if a claim with
4
+ * the same claimId was already recorded.
5
+ */
6
+ export declare function appendClaim(claim: DeliveryClaim): DeliveryClaim;
7
+ /** All claims recorded against one seller, oldest first. */
8
+ export declare function claimsForSeller(sellerAddress: string): DeliveryClaim[];
9
+ /** Every claim in the ledger, oldest first. Mainly useful for tests/inspection. */
10
+ export declare function allClaims(): DeliveryClaim[];
11
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAYA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAuBtE;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,CAQ/D;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,aAAa,EAAE,CAItE;AAED,mFAAmF;AACnF,wBAAgB,SAAS,IAAI,aAAa,EAAE,CAE3C"}
package/dist/ledger.js ADDED
@@ -0,0 +1,57 @@
1
+ // ledger.ts — append-only JSONL storage for delivery claims.
2
+ //
3
+ // Same pattern as mcp-paywall/src/ledger.mjs: one append-only file, nothing
4
+ // ever rewritten or deleted. There is intentionally no update/delete
5
+ // function anywhere in this module — a claim, once written, is permanent
6
+ // history. A second claim with the same content-addressed claimId (i.e. an
7
+ // attempt to re-record the exact same claim) is rejected rather than
8
+ // silently duplicated.
9
+ import { existsSync, readFileSync, appendFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { dataDir, ensureDataDir } from "./config.js";
12
+ import { DeliveryClaimSchema } from "./schema.js";
13
+ function claimsFile() {
14
+ return join(dataDir(), "claims.jsonl");
15
+ }
16
+ function readClaims() {
17
+ const file = claimsFile();
18
+ if (!existsSync(file))
19
+ return [];
20
+ return readFileSync(file, "utf8")
21
+ .split("\n")
22
+ .filter(Boolean)
23
+ .flatMap((line) => {
24
+ try {
25
+ return [DeliveryClaimSchema.parse(JSON.parse(line))];
26
+ }
27
+ catch {
28
+ // A corrupt/partial line (e.g. a truncated write) must not take down
29
+ // reads of the rest of the ledger.
30
+ return [];
31
+ }
32
+ });
33
+ }
34
+ /**
35
+ * Append one already-verified claim to the ledger. Throws if a claim with
36
+ * the same claimId was already recorded.
37
+ */
38
+ export function appendClaim(claim) {
39
+ const existing = readClaims();
40
+ if (existing.some((c) => c.claimId.toLowerCase() === claim.claimId.toLowerCase())) {
41
+ throw new Error(`claim_already_recorded: ${claim.claimId}`);
42
+ }
43
+ ensureDataDir();
44
+ appendFileSync(claimsFile(), JSON.stringify(claim) + "\n");
45
+ return claim;
46
+ }
47
+ /** All claims recorded against one seller, oldest first. */
48
+ export function claimsForSeller(sellerAddress) {
49
+ return readClaims()
50
+ .filter((c) => c.sellerAddress.toLowerCase() === sellerAddress.toLowerCase())
51
+ .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
52
+ }
53
+ /** Every claim in the ledger, oldest first. Mainly useful for tests/inspection. */
54
+ export function allClaims() {
55
+ return readClaims().sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
56
+ }
57
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,4EAA4E;AAC5E,qEAAqE;AACrE,yEAAyE;AACzE,2EAA2E;AAC3E,qEAAqE;AACrE,uBAAuB;AAEvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AAEtE,SAAS,UAAU;IACjB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC;SAC9B,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,OAAO,CAAC;SACf,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,IAAI,CAAC;YACH,OAAO,CAAC,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;YACrE,mCAAmC;YACnC,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAoB;IAC9C,MAAM,QAAQ,GAAG,UAAU,EAAE,CAAC;IAC9B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,KAAK,CAAC,2BAA2B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,aAAa,EAAE,CAAC;IAChB,cAAc,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,eAAe,CAAC,aAAqB;IACnD,OAAO,UAAU,EAAE;SAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,EAAE,KAAK,aAAa,CAAC,WAAW,EAAE,CAAC;SAC5E,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,SAAS;IACvB,OAAO,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACxF,CAAC"}
@@ -0,0 +1,68 @@
1
+ import * as z from "zod/v4";
2
+ export declare const ASSET_TYPES: readonly ["gpu-hours", "storage", "api-credits", "bandwidth"];
3
+ export type AssetType = (typeof ASSET_TYPES)[number];
4
+ export declare const DELIVERED_VALUES: readonly ["yes", "no", "partial"];
5
+ export type Delivered = (typeof DELIVERED_VALUES)[number];
6
+ /**
7
+ * The part of a claim that is hashed to produce claimId and that gets
8
+ * signed. Everything in here is chosen by the buyer at claim-creation time;
9
+ * claimId and signature (in DeliveryClaimSchema below) are derived from it.
10
+ */
11
+ export declare const ClaimContentSchema: z.ZodObject<{
12
+ sellerAddress: z.ZodString;
13
+ buyerAddress: z.ZodString;
14
+ assetType: z.ZodEnum<{
15
+ "gpu-hours": "gpu-hours";
16
+ storage: "storage";
17
+ "api-credits": "api-credits";
18
+ bandwidth: "bandwidth";
19
+ }>;
20
+ promisedSpec: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
21
+ delivered: z.ZodEnum<{
22
+ yes: "yes";
23
+ no: "no";
24
+ partial: "partial";
25
+ }>;
26
+ evidenceHash: z.ZodString;
27
+ settlementRef: z.ZodString;
28
+ timestamp: z.ZodString;
29
+ }, z.core.$strip>;
30
+ export type ClaimContent = z.infer<typeof ClaimContentSchema>;
31
+ /** The full, stored, signed claim — ClaimContent plus its content-address and signature. */
32
+ export declare const DeliveryClaimSchema: z.ZodObject<{
33
+ sellerAddress: z.ZodString;
34
+ buyerAddress: z.ZodString;
35
+ assetType: z.ZodEnum<{
36
+ "gpu-hours": "gpu-hours";
37
+ storage: "storage";
38
+ "api-credits": "api-credits";
39
+ bandwidth: "bandwidth";
40
+ }>;
41
+ promisedSpec: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>]>;
42
+ delivered: z.ZodEnum<{
43
+ yes: "yes";
44
+ no: "no";
45
+ partial: "partial";
46
+ }>;
47
+ evidenceHash: z.ZodString;
48
+ settlementRef: z.ZodString;
49
+ timestamp: z.ZodString;
50
+ claimId: z.ZodString;
51
+ signature: z.ZodString;
52
+ }, z.core.$strip>;
53
+ export type DeliveryClaim = z.infer<typeof DeliveryClaimSchema>;
54
+ /**
55
+ * Deterministic JSON stringify: object keys sorted recursively so hashing
56
+ * and signing are stable regardless of the original key insertion order.
57
+ * Arrays keep their order (order is meaningful there); only object keys are
58
+ * sorted.
59
+ */
60
+ export declare function canonicalize(value: unknown): string;
61
+ /**
62
+ * Content-addressed id for a claim: sha256 of the canonical JSON of its
63
+ * content fields (everything except claimId and signature themselves).
64
+ * Validates+normalizes `content` first so a claimId can never be computed
65
+ * over a shape that wouldn't itself pass ClaimContentSchema.
66
+ */
67
+ export declare function computeClaimId(content: ClaimContent): string;
68
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAG5B,eAAO,MAAM,WAAW,+DAAgE,CAAC;AACzF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,eAAO,MAAM,gBAAgB,mCAAoC,CAAC;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAS1D;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;iBA0B7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;iBAS9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAeD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAI5D"}
package/dist/schema.js ADDED
@@ -0,0 +1,99 @@
1
+ // schema.ts — the DeliveryClaim shape.
2
+ //
3
+ // A DeliveryClaim is a factual, signed receipt: "buyer X paid seller Y for
4
+ // asset Z under x402 settlement R, and here is whether what was promised
5
+ // actually arrived." It is deliberately NOT a score, rating, or judgment —
6
+ // see README.md "Wat dit NIET is".
7
+ //
8
+ // GUARDRAIL (halal): assetType is a closed enum of physical/compute capacity
9
+ // kinds. Do not add anything that looks like a financial instrument (credit,
10
+ // loan, yield, interest-bearing balance, invoice-financing) to this file —
11
+ // see the "HALAL GUARDRAILS" section of the project brief. If a field or enum
12
+ // value even smells like that, leave it out.
13
+ import * as z from "zod/v4";
14
+ import { createHash } from "node:crypto";
15
+ export const ASSET_TYPES = ["gpu-hours", "storage", "api-credits", "bandwidth"];
16
+ export const DELIVERED_VALUES = ["yes", "no", "partial"];
17
+ const ETH_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
18
+ const SHA256_HEX_RE = /^[0-9a-fA-F]{64}$/;
19
+ const CLAIM_ID_RE = /^0x[0-9a-fA-F]{64}$/;
20
+ // 65-byte ECDSA signature (r ++ s ++ v), hex-encoded with 0x prefix — the
21
+ // shape ethers.Signer#signMessage() / ethers.verifyMessage() produce.
22
+ const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
23
+ /**
24
+ * The part of a claim that is hashed to produce claimId and that gets
25
+ * signed. Everything in here is chosen by the buyer at claim-creation time;
26
+ * claimId and signature (in DeliveryClaimSchema below) are derived from it.
27
+ */
28
+ export const ClaimContentSchema = z.object({
29
+ sellerAddress: z
30
+ .string()
31
+ .regex(ETH_ADDRESS_RE, "sellerAddress must be a 0x-prefixed 20-byte address")
32
+ .describe("0x address of the agent/service that was paid and was supposed to deliver"),
33
+ buyerAddress: z
34
+ .string()
35
+ .regex(ETH_ADDRESS_RE, "buyerAddress must be a 0x-prefixed 20-byte address")
36
+ .describe("0x address of the paying agent — must match the address recovered from `signature`"),
37
+ assetType: z.enum(ASSET_TYPES).describe("What kind of capacity this claim is about"),
38
+ promisedSpec: z
39
+ .union([z.string().min(1), z.record(z.string(), z.unknown())])
40
+ .describe("What the seller promised to deliver — free text or a structured object"),
41
+ delivered: z.enum(DELIVERED_VALUES).describe("Whether what was promised actually arrived"),
42
+ evidenceHash: z
43
+ .string()
44
+ .regex(SHA256_HEX_RE, "evidenceHash must be a sha256 hex digest (64 hex chars, no 0x prefix)")
45
+ .describe("sha256 hex digest of evidence for this claim (logs, response payload, etc.) — the evidence itself is not stored here"),
46
+ settlementRef: z
47
+ .string()
48
+ .min(1, "settlementRef is required (x402 payment reference or on-chain tx hash)")
49
+ .describe("x402 payment reference or on-chain tx hash for the settlement this claim is about"),
50
+ timestamp: z
51
+ .string()
52
+ .refine((v) => !Number.isNaN(Date.parse(v)), "timestamp must be a valid ISO-8601 date string")
53
+ .describe("ISO-8601 timestamp of when this claim was made"),
54
+ });
55
+ /** The full, stored, signed claim — ClaimContent plus its content-address and signature. */
56
+ export const DeliveryClaimSchema = ClaimContentSchema.extend({
57
+ claimId: z
58
+ .string()
59
+ .regex(CLAIM_ID_RE, "claimId must be a 0x-prefixed sha256 hex digest")
60
+ .describe("Content-addressed id: sha256 of the canonical JSON of this claim's content fields — see computeClaimId()"),
61
+ signature: z
62
+ .string()
63
+ .regex(SIGNATURE_RE, "signature must be a 0x-prefixed 65-byte ECDSA signature")
64
+ .describe("Buyer's EIP-191 personal-sign signature over claimId"),
65
+ });
66
+ /**
67
+ * Deterministic JSON stringify: object keys sorted recursively so hashing
68
+ * and signing are stable regardless of the original key insertion order.
69
+ * Arrays keep their order (order is meaningful there); only object keys are
70
+ * sorted.
71
+ */
72
+ export function canonicalize(value) {
73
+ return JSON.stringify(sortKeysDeep(value));
74
+ }
75
+ function sortKeysDeep(value) {
76
+ if (Array.isArray(value))
77
+ return value.map(sortKeysDeep);
78
+ if (value !== null && typeof value === "object") {
79
+ const input = value;
80
+ const out = {};
81
+ for (const key of Object.keys(input).sort()) {
82
+ out[key] = sortKeysDeep(input[key]);
83
+ }
84
+ return out;
85
+ }
86
+ return value;
87
+ }
88
+ /**
89
+ * Content-addressed id for a claim: sha256 of the canonical JSON of its
90
+ * content fields (everything except claimId and signature themselves).
91
+ * Validates+normalizes `content` first so a claimId can never be computed
92
+ * over a shape that wouldn't itself pass ClaimContentSchema.
93
+ */
94
+ export function computeClaimId(content) {
95
+ const parsed = ClaimContentSchema.parse(content);
96
+ const hash = createHash("sha256").update(canonicalize(parsed)).digest("hex");
97
+ return `0x${hash}`;
98
+ }
99
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { ethers } from "ethers";
2
+ import { type ClaimContent } from "./schema.js";
3
+ export interface SignedClaimParts {
4
+ claimId: string;
5
+ signature: string;
6
+ }
7
+ /**
8
+ * Sign a delivery claim as the buyer. Returns the content-addressed claimId
9
+ * alongside the signature so callers can assemble a full DeliveryClaim.
10
+ */
11
+ export declare function signClaim(signer: ethers.Signer, content: ClaimContent): Promise<SignedClaimParts>;
12
+ /**
13
+ * Recover the address that produced `signature` over `claimId`. Pure,
14
+ * offline, deterministic — no network call.
15
+ */
16
+ export declare function recoverClaimSigner(claimId: string, signature: string): string;
17
+ export type VerifyResult = {
18
+ ok: true;
19
+ } | {
20
+ ok: false;
21
+ reason: string;
22
+ };
23
+ /**
24
+ * Verify that an assembled claim is internally consistent:
25
+ * 1. claimId really is the sha256 content-address of the claim's own
26
+ * content fields (catches a submitted claimId that doesn't match a
27
+ * tampered/edited content field), and
28
+ * 2. the signature really does recover to buyerAddress — the party
29
+ * attesting to what it received, per the project's design (the PAYING
30
+ * agent leaves the claim about the SELLER it paid).
31
+ */
32
+ export declare function verifyClaim(claim: ClaimContent & {
33
+ claimId: string;
34
+ signature: string;
35
+ }): VerifyResult;
36
+ //# sourceMappingURL=signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAIvG;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC3D,YAAY,CAkBd"}
@@ -0,0 +1,54 @@
1
+ // signing.ts — sign a claim as the buyer, and verify that signature later.
2
+ //
3
+ // Deliberately simple EIP-191 personal-sign over the claim's content-address
4
+ // (claimId), not EIP-712 typed data. This keeps the MVP's crypto surface
5
+ // small and easy to audit end-to-end; a typed-data path can be added later
6
+ // as an additive upgrade (accept both, prefer the new one) without breaking
7
+ // already-recorded claims. See mcp-paywall/src/x402.mjs for the EIP-712
8
+ // pattern this project can graduate to if/when claims need domain-separated
9
+ // signing (e.g. once claims are also submitted on-chain).
10
+ import { ethers } from "ethers";
11
+ import { computeClaimId } from "./schema.js";
12
+ /**
13
+ * Sign a delivery claim as the buyer. Returns the content-addressed claimId
14
+ * alongside the signature so callers can assemble a full DeliveryClaim.
15
+ */
16
+ export async function signClaim(signer, content) {
17
+ const claimId = computeClaimId(content);
18
+ const signature = await signer.signMessage(claimId);
19
+ return { claimId, signature };
20
+ }
21
+ /**
22
+ * Recover the address that produced `signature` over `claimId`. Pure,
23
+ * offline, deterministic — no network call.
24
+ */
25
+ export function recoverClaimSigner(claimId, signature) {
26
+ return ethers.verifyMessage(claimId, signature);
27
+ }
28
+ /**
29
+ * Verify that an assembled claim is internally consistent:
30
+ * 1. claimId really is the sha256 content-address of the claim's own
31
+ * content fields (catches a submitted claimId that doesn't match a
32
+ * tampered/edited content field), and
33
+ * 2. the signature really does recover to buyerAddress — the party
34
+ * attesting to what it received, per the project's design (the PAYING
35
+ * agent leaves the claim about the SELLER it paid).
36
+ */
37
+ export function verifyClaim(claim) {
38
+ const expectedClaimId = computeClaimId(claim);
39
+ if (expectedClaimId.toLowerCase() !== claim.claimId.toLowerCase()) {
40
+ return { ok: false, reason: "claimId_mismatch" };
41
+ }
42
+ let recovered;
43
+ try {
44
+ recovered = recoverClaimSigner(claim.claimId, claim.signature);
45
+ }
46
+ catch (e) {
47
+ return { ok: false, reason: `signature_recovery_failed: ${e.message}` };
48
+ }
49
+ if (recovered.toLowerCase() !== claim.buyerAddress.toLowerCase()) {
50
+ return { ok: false, reason: "signature_does_not_match_buyer" };
51
+ }
52
+ return { ok: true };
53
+ }
54
+ //# sourceMappingURL=signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.js","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,EAAE;AACF,6EAA6E;AAC7E,yEAAyE;AACzE,2EAA2E;AAC3E,4EAA4E;AAC5E,wEAAwE;AACxE,4EAA4E;AAC5E,0DAA0D;AAE1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAqB,cAAc,EAAE,MAAM,aAAa,CAAC;AAOhE;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAqB,EAAE,OAAqB;IAC1E,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACpD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,SAAiB;IACnE,OAAO,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AAClD,CAAC;AAID;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CACzB,KAA4D;IAE5D,MAAM,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,eAAe,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;QAClE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IACnD,CAAC;IAED,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,GAAG,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,8BAA+B,CAAW,CAAC,OAAO,EAAE,EAAE,CAAC;IACrF,CAAC;IAED,IAAI,SAAS,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,EAAE,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,gCAAgC,EAAE,CAAC;IACjE,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { ethers } from "ethers";
2
+ import type { ClaimContent, DeliveryClaim } from "./schema.js";
3
+ /** A fresh random TEST wallet — never funded, never used outside this test suite. */
4
+ export declare function testWallet(): ethers.HDNodeWallet;
5
+ /** sha256 hex digest of some evidence bytes, in the shape evidenceHash expects. */
6
+ export declare function evidenceHash(evidence: string): string;
7
+ export interface BuildClaimOverrides {
8
+ sellerAddress?: string;
9
+ buyerAddress?: string;
10
+ assetType?: ClaimContent["assetType"];
11
+ promisedSpec?: ClaimContent["promisedSpec"];
12
+ delivered?: ClaimContent["delivered"];
13
+ evidenceHash?: string;
14
+ settlementRef?: string;
15
+ timestamp?: string;
16
+ }
17
+ /** Build a valid, signed DeliveryClaim for buyer `wallet`, with sensible defaults. */
18
+ export declare function buildSignedClaim(wallet: ethers.HDNodeWallet, overrides?: BuildClaimOverrides): Promise<DeliveryClaim>;
19
+ //# sourceMappingURL=test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG/D,qFAAqF;AACrF,wBAAgB,UAAU,IAAI,MAAM,CAAC,YAAY,CAEhD;AAED,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,YAAY,CAAC,cAAc,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,sFAAsF;AACtF,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,CAAC,YAAY,EAC3B,SAAS,GAAE,mBAAwB,GAClC,OAAO,CAAC,aAAa,CAAC,CAaxB"}
@@ -0,0 +1,30 @@
1
+ // test-helpers.ts — shared fixtures for the test suite. Not part of the
2
+ // published surface (README/dist only ship what package.json's "files"
3
+ // lists), but kept in src/ so it type-checks alongside the code it exercises.
4
+ import { ethers } from "ethers";
5
+ import { createHash } from "node:crypto";
6
+ import { signClaim } from "./signing.js";
7
+ /** A fresh random TEST wallet — never funded, never used outside this test suite. */
8
+ export function testWallet() {
9
+ return ethers.Wallet.createRandom();
10
+ }
11
+ /** sha256 hex digest of some evidence bytes, in the shape evidenceHash expects. */
12
+ export function evidenceHash(evidence) {
13
+ return createHash("sha256").update(evidence).digest("hex");
14
+ }
15
+ /** Build a valid, signed DeliveryClaim for buyer `wallet`, with sensible defaults. */
16
+ export async function buildSignedClaim(wallet, overrides = {}) {
17
+ const content = {
18
+ sellerAddress: overrides.sellerAddress ?? "0x00000000000000000000000000000000000000aa",
19
+ buyerAddress: overrides.buyerAddress ?? wallet.address,
20
+ assetType: overrides.assetType ?? "gpu-hours",
21
+ promisedSpec: overrides.promisedSpec ?? "1x A100, 4 hours, us-east region",
22
+ delivered: overrides.delivered ?? "yes",
23
+ evidenceHash: overrides.evidenceHash ?? evidenceHash("demo evidence payload"),
24
+ settlementRef: overrides.settlementRef ?? "0x" + "11".repeat(32),
25
+ timestamp: overrides.timestamp ?? new Date().toISOString(),
26
+ };
27
+ const { claimId, signature } = await signClaim(wallet, content);
28
+ return { ...content, claimId, signature };
29
+ }
30
+ //# sourceMappingURL=test-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,uEAAuE;AACvE,8EAA8E;AAE9E,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,qFAAqF;AACrF,MAAM,UAAU,UAAU;IACxB,OAAO,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;AACtC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAaD,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAA2B,EAC3B,YAAiC,EAAE;IAEnC,MAAM,OAAO,GAAiB;QAC5B,aAAa,EAAE,SAAS,CAAC,aAAa,IAAI,4CAA4C;QACtF,YAAY,EAAE,SAAS,CAAC,YAAY,IAAI,MAAM,CAAC,OAAO;QACtD,SAAS,EAAE,SAAS,CAAC,SAAS,IAAI,WAAW;QAC7C,YAAY,EAAE,SAAS,CAAC,YAAY,IAAI,kCAAkC;QAC1E,SAAS,EAAE,SAAS,CAAC,SAAS,IAAI,KAAK;QACvC,YAAY,EAAE,SAAS,CAAC,YAAY,IAAI,YAAY,CAAC,uBAAuB,CAAC;QAC7E,aAAa,EAAE,SAAS,CAAC,aAAa,IAAI,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAChE,SAAS,EAAE,SAAS,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KAC3D,CAAC;IACF,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChE,OAAO,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { type DeliveryClaim } from "./schema.js";
2
+ export type RecordDeliveryResult = {
3
+ ok: true;
4
+ claimId: string;
5
+ } | {
6
+ ok: false;
7
+ reason: string;
8
+ };
9
+ /**
10
+ * Validate the claim's schema, verify its signature (must recover to
11
+ * buyerAddress), and persist it to the append-only ledger. This is the
12
+ * logic behind the `record_delivery` MCP tool.
13
+ */
14
+ export declare function recordDelivery(input: unknown): RecordDeliveryResult;
15
+ export interface DeliveryHistoryResult {
16
+ sellerAddress: string;
17
+ count: number;
18
+ claims: DeliveryClaim[];
19
+ }
20
+ /**
21
+ * Every known, signature-verified claim recorded against sellerAddress,
22
+ * oldest first. Purely factual — no aggregate score, rating, or reputation
23
+ * judgment is computed here; see README.md "Wat dit NIET is". This is the
24
+ * logic behind the `get_delivery_history` MCP tool.
25
+ */
26
+ export declare function getDeliveryHistory(sellerAddress: string): DeliveryHistoryResult;
27
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAMA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAItE,MAAM,MAAM,oBAAoB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjG;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAmBnE;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,qBAAqB,CAG/E"}
package/dist/tools.js ADDED
@@ -0,0 +1,42 @@
1
+ // tools.ts — the two tools' actual logic, kept separate from the MCP
2
+ // transport wiring in index.ts (same separation-of-concerns pattern as
3
+ // al-yad-mcp-server's companion-client.ts vs. index.ts). This lets tests
4
+ // and examples/demo.ts call the real logic directly without spinning up a
5
+ // stdio JSON-RPC transport.
6
+ import { DeliveryClaimSchema } from "./schema.js";
7
+ import { verifyClaim } from "./signing.js";
8
+ import { appendClaim, claimsForSeller } from "./ledger.js";
9
+ /**
10
+ * Validate the claim's schema, verify its signature (must recover to
11
+ * buyerAddress), and persist it to the append-only ledger. This is the
12
+ * logic behind the `record_delivery` MCP tool.
13
+ */
14
+ export function recordDelivery(input) {
15
+ const parsed = DeliveryClaimSchema.safeParse(input);
16
+ if (!parsed.success) {
17
+ return { ok: false, reason: `invalid_claim: ${parsed.error.issues.map((i) => i.message).join("; ")}` };
18
+ }
19
+ const claim = parsed.data;
20
+ const verdict = verifyClaim(claim);
21
+ if (!verdict.ok) {
22
+ return { ok: false, reason: `signature_invalid: ${verdict.reason}` };
23
+ }
24
+ try {
25
+ appendClaim(claim);
26
+ }
27
+ catch (e) {
28
+ return { ok: false, reason: e.message };
29
+ }
30
+ return { ok: true, claimId: claim.claimId };
31
+ }
32
+ /**
33
+ * Every known, signature-verified claim recorded against sellerAddress,
34
+ * oldest first. Purely factual — no aggregate score, rating, or reputation
35
+ * judgment is computed here; see README.md "Wat dit NIET is". This is the
36
+ * logic behind the `get_delivery_history` MCP tool.
37
+ */
38
+ export function getDeliveryHistory(sellerAddress) {
39
+ const claims = claimsForSeller(sellerAddress);
40
+ return { sellerAddress, count: claims.length, claims };
41
+ }
42
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,uEAAuE;AACvE,yEAAyE;AACzE,0EAA0E;AAC1E,4BAA4B;AAE5B,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI3D;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;IACzG,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;IAC1B,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IACvE,CAAC;IAED,IAAI,CAAC;QACH,WAAW,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;AAC9C,CAAC;AAQD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,aAAqB;IACtD,MAAM,MAAM,GAAG,eAAe,CAAC,aAAa,CAAC,CAAC;IAC9C,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;AACzD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "capacity-attest",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for capacity-delivery attestations: after an x402 payment for compute capacity (GPU-hours, storage, API credits, bandwidth), the paying agent leaves a signed, factual claim of what was actually delivered — so other agents can check a seller's delivery history before paying. No reputation score, no judgment, no financial instrument.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "x402",
9
+ "agent-to-agent",
10
+ "agent-commerce",
11
+ "capacity-trade",
12
+ "attestation",
13
+ "provenance",
14
+ "ai-agent"
15
+ ],
16
+ "author": "holistis",
17
+ "license": "MIT",
18
+ "homepage": "https://tokenizen.nl",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/holistis/tokenizen.git",
22
+ "directory": "packages/capacity-attest"
23
+ },
24
+ "mcpName": "io.github.holistis/capacity-attest",
25
+ "type": "module",
26
+ "main": "./dist/index.js",
27
+ "bin": {
28
+ "capacity-attest": "./dist/index.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "test": "vitest run",
41
+ "start": "node dist/index.js",
42
+ "demo": "tsx examples/demo.ts"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.30.0",
46
+ "ethers": "^6.13.0",
47
+ "zod": "^4.5.2"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.10.5",
51
+ "tsx": "^4.19.2",
52
+ "typescript": "^5.7.3",
53
+ "vitest": "^2.1.8"
54
+ }
55
+ }