From 485deb383fa096c6ffd1930dac6b393ebb590ddf Mon Sep 17 00:00:00 2001 From: t49qnsx7qt-kpanks Date: Thu, 2 Apr 2026 01:21:28 -0500 Subject: [PATCH] feat: add @x402/mnemopay middleware for AI agent economic memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI agents using x402 today are stateless — they pay and forget. This package layers MnemoPay's economic memory on top of x402 payment flows so agents remember costs, track endpoint reliability, and build reputation from real payment outcomes. - withMnemoPay() wraps any x402-enabled fetch with memory operations - Before each request: recall() checks past payment experiences - On success: settle() reinforces the positive memory (+reputation) - On failure: refund() docks the memory (-reputation) - recallEndpointInsight() surfaces cost history and success rates - Full test suite with 14 tests covering all payment flow paths - Follows existing x402 package structure (tsup, vitest, eslint) Co-Authored-By: Claude Opus 4.6 --- .../packages/http/mnemopay/CHANGELOG.md | 11 + typescript/packages/http/mnemopay/README.md | 159 +++++++++ .../packages/http/mnemopay/eslint.config.js | 77 +++++ .../packages/http/mnemopay/package.json | 74 +++++ .../packages/http/mnemopay/src/index.ts | 24 ++ .../http/mnemopay/src/middleware.test.ts | 273 ++++++++++++++++ .../packages/http/mnemopay/src/middleware.ts | 303 ++++++++++++++++++ .../packages/http/mnemopay/src/types.ts | 155 +++++++++ .../packages/http/mnemopay/tsconfig.json | 9 + .../packages/http/mnemopay/tsup.config.ts | 27 ++ .../packages/http/mnemopay/vitest.config.ts | 10 + 11 files changed, 1122 insertions(+) create mode 100644 typescript/packages/http/mnemopay/CHANGELOG.md create mode 100644 typescript/packages/http/mnemopay/README.md create mode 100644 typescript/packages/http/mnemopay/eslint.config.js create mode 100644 typescript/packages/http/mnemopay/package.json create mode 100644 typescript/packages/http/mnemopay/src/index.ts create mode 100644 typescript/packages/http/mnemopay/src/middleware.test.ts create mode 100644 typescript/packages/http/mnemopay/src/middleware.ts create mode 100644 typescript/packages/http/mnemopay/src/types.ts create mode 100644 typescript/packages/http/mnemopay/tsconfig.json create mode 100644 typescript/packages/http/mnemopay/tsup.config.ts create mode 100644 typescript/packages/http/mnemopay/vitest.config.ts diff --git a/typescript/packages/http/mnemopay/CHANGELOG.md b/typescript/packages/http/mnemopay/CHANGELOG.md new file mode 100644 index 0000000000..79e7f45611 --- /dev/null +++ b/typescript/packages/http/mnemopay/CHANGELOG.md @@ -0,0 +1,11 @@ +# @x402/mnemopay Changelog + +## 2.8.0 + +### Minor Changes + +- Initial release: MnemoPay middleware for x402 payment protocol +- `withMnemoPay()` wrapper adds economic memory to any x402-enabled fetch +- `recallEndpointInsight()` queries agent memory for endpoint cost/reliability data +- `rememberPaymentOutcome()` stores payment results for future reference +- Automatic settle/refund flow based on payment success/failure diff --git a/typescript/packages/http/mnemopay/README.md b/typescript/packages/http/mnemopay/README.md new file mode 100644 index 0000000000..1b82a3a895 --- /dev/null +++ b/typescript/packages/http/mnemopay/README.md @@ -0,0 +1,159 @@ +# @x402/mnemopay + +MnemoPay middleware for x402 — turns "pay and forget" into "pay and learn." + +Standard x402 handles payments mechanically: see a 402, pay, get the resource. The agent never learns. It pays the same price to unreliable endpoints, never discovers cheaper alternatives, and has no memory of what worked. + +This package layers **economic memory** on top of x402. After each payment, the AI agent remembers the cost, the endpoint, and whether it succeeded. Before each request, it recalls past experiences — surfacing cheaper alternatives, flagging unreliable endpoints, and building a reputation score that reflects real payment outcomes. + +## Installation + +```bash +pnpm install @x402/mnemopay @mnemopay/sdk +``` + +## Quick Start + +```typescript +import { wrapFetchWithPayment, x402Client } from "@x402/fetch"; +import { ExactEvmScheme } from "@x402/evm"; +import { MnemoPayLite } from "@mnemopay/sdk"; +import { withMnemoPay } from "@x402/mnemopay"; +import { privateKeyToAccount } from "viem/accounts"; + +// 1. Set up x402 payment as usual +const account = privateKeyToAccount("0xYourPrivateKey"); +const client = new x402Client().register("eip155:8453", new ExactEvmScheme(account)); +const payFetch = wrapFetchWithPayment(fetch, client); + +// 2. Add MnemoPay memory layer +const agent = new MnemoPayLite("my-agent", 0.05); +const smartFetch = withMnemoPay(payFetch, { agent }); + +// 3. Use it — the agent now remembers every payment +const response = await smartFetch("https://api.example.com/paid-endpoint"); +// Agent stored: "x402 payment to https://api.example.com/paid-endpoint: success, cost: $0.02" + +// Next time, it recalls that memory before paying +const response2 = await smartFetch("https://api.example.com/paid-endpoint"); +// Agent recalled: 1 memory, success rate: 100%, avg cost: $0.02 +``` + +## How It Works + +The middleware wraps any fetch function (ideally one already wrapped with `@x402/fetch`) and adds four memory operations around each request: + +| Phase | What happens | MnemoPay API | +|-------|-------------|--------------| +| **Before request** | Recall past payment experiences with this endpoint | `agent.recall()` | +| **After 402 payment** | Record the charge amount and endpoint | `agent.charge()` | +| **On success** | Settle the transaction, reinforcing the positive memory | `agent.settle()` | +| **On failure** | Refund the transaction, docking reputation | `agent.refund()` | + +Over time, the agent builds a knowledge base of: +- Which endpoints are cheap vs. expensive +- Which endpoints are reliable vs. flaky +- How costs change over time +- Which alternatives exist for the same resource + +## API + +### `withMnemoPay(fetchFn, config)` + +The primary wrapper. Layers memory on top of an existing fetch function. + +```typescript +const smartFetch = withMnemoPay(payFetch, { + agent: mnemoPayAgent, // Required: MnemoPay agent instance + recallLimit: 5, // Optional: max memories to recall per request (default: 5) + reliabilityThreshold: 0.3, // Optional: warn below this success rate (default: 0.3) + debug: false, // Optional: log memory events to console (default: false) +}); +``` + +### `withMnemoPayAgent(fetchFn, agent)` + +Convenience wrapper with default configuration. + +```typescript +const smartFetch = withMnemoPayAgent(payFetch, agent); +``` + +### `recallEndpointInsight(agent, endpoint, limit?)` + +Manually query the agent's memory about a specific endpoint. + +```typescript +import { recallEndpointInsight } from "@x402/mnemopay"; + +const insight = await recallEndpointInsight(agent, "https://api.example.com/paid"); +if (insight) { + console.log(`Success rate: ${insight.successRate}`); + console.log(`Average cost: $${insight.averageCost}`); + console.log(`Interactions: ${insight.interactionCount}`); +} +``` + +### `rememberPaymentOutcome(agent, result, debug?)` + +Manually record a payment outcome (useful for custom payment flows). + +```typescript +import { rememberPaymentOutcome } from "@x402/mnemopay"; + +await rememberPaymentOutcome(agent, { + success: true, + transactionId: "tx-123", + endpoint: "https://api.example.com/paid", + cost: 0.05, +}); +``` + +## Custom MnemoPay Agent + +You don't need the full `@mnemopay/sdk`. Any object implementing the `MnemoPayAgent` interface works: + +```typescript +import type { MnemoPayAgent } from "@x402/mnemopay"; + +const myAgent: MnemoPayAgent = { + async remember(content, options) { /* store in your DB */ }, + async recall(query, limit) { /* search your DB */ return []; }, + async charge(amount, description) { /* record payment */ return "tx-id"; }, + async settle(txId) { /* mark as settled */ }, + async refund(txId) { /* mark as refunded */ }, + balance() { return { wallet: 100, reputation: 0.9 }; }, +}; + +const smartFetch = withMnemoPay(payFetch, { agent: myAgent }); +``` + +## Debug Mode + +Enable debug logging to see the agent's memory operations: + +```typescript +const smartFetch = withMnemoPay(payFetch, { + agent, + debug: true, +}); + +// Console output: +// [mnemopay] recalled 3 memories for https://api.example.com/paid, success rate: 67%, avg cost: $0.04 +// [mnemopay] WARNING: https://api.example.com/flaky has low reliability (20%). Consider alternatives. +// [mnemopay] settled tx tx-123 for https://api.example.com/paid +``` + +## Why This Matters for AI Agents + +AI agents making API calls through x402 today are stateless — they pay whatever is asked, every time, with no memory of past outcomes. This is like a human who forgets the price of groceries every time they walk into a store. + +With MnemoPay integration, x402 agents develop **economic intelligence**: + +- **Cost awareness**: "This endpoint usually costs $0.02, but today it's asking for $0.10 — something changed" +- **Reliability tracking**: "This endpoint fails 40% of the time — I should prefer the alternative" +- **Reputation building**: Agents that consistently make good payment decisions build higher reputation scores, which other agents and services can use as a trust signal + +## License + +Apache-2.0 diff --git a/typescript/packages/http/mnemopay/eslint.config.js b/typescript/packages/http/mnemopay/eslint.config.js new file mode 100644 index 0000000000..7ef2d384d0 --- /dev/null +++ b/typescript/packages/http/mnemopay/eslint.config.js @@ -0,0 +1,77 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + RequestInfo: "readonly", + RequestInit: "readonly", + Response: "readonly", + Headers: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + console: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/mnemopay/package.json b/typescript/packages/http/mnemopay/package.json new file mode 100644 index 0000000000..feea5c1431 --- /dev/null +++ b/typescript/packages/http/mnemopay/package.json @@ -0,0 +1,74 @@ +{ + "name": "@x402/mnemopay", + "version": "2.8.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [ + "x402", + "payment", + "protocol", + "mnemopay", + "ai", + "agent", + "memory" + ], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 MnemoPay middleware — AI agents that remember payment outcomes and learn from them", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@x402/core": "workspace:~" + }, + "peerDependencies": { + "@mnemopay/sdk": ">=0.1.0" + }, + "peerDependenciesMeta": { + "@mnemopay/sdk": { + "optional": false + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/http/mnemopay/src/index.ts b/typescript/packages/http/mnemopay/src/index.ts new file mode 100644 index 0000000000..226493d45d --- /dev/null +++ b/typescript/packages/http/mnemopay/src/index.ts @@ -0,0 +1,24 @@ +export { withMnemoPay, withMnemoPayAgent, recallEndpointInsight, rememberPaymentOutcome } from "./middleware"; +export type { + MnemoPayAgent, + MnemoPayMiddlewareConfig, + MnemoPayMemory, + MnemoPayPaymentResult, + EndpointInsight, +} from "./types"; + +// Re-export x402 core types for convenience +export { x402Client, x402HTTPClient } from "@x402/core/client"; +export type { + PaymentPolicy, + SchemeRegistration, + SelectPaymentRequirements, + x402ClientConfig, +} from "@x402/core/client"; +export type { + Network, + PaymentPayload, + PaymentRequired, + PaymentRequirements, + SchemeNetworkClient, +} from "@x402/core/types"; diff --git a/typescript/packages/http/mnemopay/src/middleware.test.ts b/typescript/packages/http/mnemopay/src/middleware.test.ts new file mode 100644 index 0000000000..3c6b89c945 --- /dev/null +++ b/typescript/packages/http/mnemopay/src/middleware.test.ts @@ -0,0 +1,273 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + withMnemoPay, + withMnemoPayAgent, + recallEndpointInsight, + rememberPaymentOutcome, +} from "./middleware"; +import type { MnemoPayAgent, MnemoPayPaymentResult } from "./types"; + +/** + * Creates a mock MnemoPay agent for testing. + * + * @returns A mock agent with spied methods. + */ +function createMockAgent(): MnemoPayAgent & { + remember: ReturnType; + recall: ReturnType; + charge: ReturnType; + settle: ReturnType; + refund: ReturnType; + balance: ReturnType; +} { + return { + remember: vi.fn().mockResolvedValue(undefined), + recall: vi.fn().mockResolvedValue([]), + charge: vi.fn().mockResolvedValue("tx-123"), + settle: vi.fn().mockResolvedValue(undefined), + refund: vi.fn().mockResolvedValue(undefined), + balance: vi.fn().mockReturnValue({ wallet: 100, reputation: 0.8 }), + }; +} + +/** + * Creates a mock fetch function that returns a configurable response. + * + * @param status - HTTP status code to return. + * @param headers - Headers to include in the response. + * @returns A mock fetch function. + */ +function createMockFetch( + status: number, + headers: Record = {}, +): typeof globalThis.fetch { + return vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: "ok" }), { + status, + headers: new Headers(headers), + }), + ); +} + +describe("withMnemoPay", () => { + let agent: ReturnType; + + beforeEach(() => { + agent = createMockAgent(); + }); + + it("should pass through non-402 responses and remember the outcome", async () => { + const mockFetch = createMockFetch(200); + const wrappedFetch = withMnemoPay(mockFetch, { agent }); + + const response = await wrappedFetch("https://api.example.com/data"); + + expect(response.status).toBe(200); + expect(agent.recall).toHaveBeenCalledWith( + "x402 payment to https://api.example.com/data", + 5, + ); + expect(agent.remember).toHaveBeenCalledWith( + expect.stringContaining("success"), + expect.objectContaining({ tags: expect.arrayContaining(["x402", "success"]) }), + ); + }); + + it("should remember failed payment outcomes", async () => { + const mockFetch = createMockFetch(402); + const wrappedFetch = withMnemoPay(mockFetch, { agent }); + + const response = await wrappedFetch("https://api.example.com/paid"); + + expect(response.status).toBe(402); + expect(agent.remember).toHaveBeenCalledWith( + expect.stringContaining("failure"), + expect.objectContaining({ tags: expect.arrayContaining(["x402", "failure"]) }), + ); + }); + + it("should settle transactions on successful payment", async () => { + const mockFetch = createMockFetch(200, { + "PAYMENT-RESPONSE": btoa(JSON.stringify({ amount: 0.05 })), + }); + const wrappedFetch = withMnemoPay(mockFetch, { agent }); + + await wrappedFetch("https://api.example.com/paid"); + + expect(agent.charge).toHaveBeenCalledWith(0.05, "x402 payment to https://api.example.com/paid"); + expect(agent.settle).toHaveBeenCalledWith("tx-123"); + }); + + it("should refund transactions on failed payment", async () => { + const mockFetch = createMockFetch(500, { + "PAYMENT-RESPONSE": btoa(JSON.stringify({ amount: 0.05 })), + }); + const wrappedFetch = withMnemoPay(mockFetch, { agent }); + + await wrappedFetch("https://api.example.com/paid"); + + expect(agent.charge).toHaveBeenCalledWith(0.05, "x402 payment to https://api.example.com/paid"); + expect(agent.refund).toHaveBeenCalledWith("tx-123"); + }); + + it("should recall memories before each request", async () => { + agent.recall.mockResolvedValue([ + { content: "x402 payment to https://api.example.com/paid: success, cost: $0.03" }, + { content: "x402 payment to https://api.example.com/paid: success, cost: $0.04" }, + ]); + const mockFetch = createMockFetch(200); + const wrappedFetch = withMnemoPay(mockFetch, { agent, recallLimit: 10 }); + + await wrappedFetch("https://api.example.com/paid"); + + expect(agent.recall).toHaveBeenCalledWith( + "x402 payment to https://api.example.com/paid", + 10, + ); + }); + + it("should handle recall failures gracefully", async () => { + agent.recall.mockRejectedValue(new Error("Memory DB offline")); + const mockFetch = createMockFetch(200); + const wrappedFetch = withMnemoPay(mockFetch, { agent }); + + const response = await wrappedFetch("https://api.example.com/data"); + + expect(response.status).toBe(200); + }); + + it("should handle remember failures gracefully", async () => { + agent.remember.mockRejectedValue(new Error("Storage full")); + const mockFetch = createMockFetch(200); + const wrappedFetch = withMnemoPay(mockFetch, { agent }); + + const response = await wrappedFetch("https://api.example.com/data"); + + expect(response.status).toBe(200); + }); + + it("should propagate fetch errors after remembering the failure", async () => { + const errorFetch = vi.fn().mockRejectedValue(new Error("Network error")); + const wrappedFetch = withMnemoPay(errorFetch as typeof globalThis.fetch, { agent }); + + await expect(wrappedFetch("https://api.example.com/data")).rejects.toThrow("Network error"); + expect(agent.remember).toHaveBeenCalledWith( + expect.stringContaining("failure"), + expect.objectContaining({ importance: 0.9 }), + ); + }); + + it("should use custom recallLimit", async () => { + const mockFetch = createMockFetch(200); + const wrappedFetch = withMnemoPay(mockFetch, { agent, recallLimit: 3 }); + + await wrappedFetch("https://api.example.com/data"); + + expect(agent.recall).toHaveBeenCalledWith(expect.any(String), 3); + }); +}); + +describe("withMnemoPayAgent", () => { + it("should create a wrapped fetch with default config", async () => { + const agent = createMockAgent(); + const mockFetch = createMockFetch(200); + const wrappedFetch = withMnemoPayAgent(mockFetch, agent); + + const response = await wrappedFetch("https://api.example.com/data"); + + expect(response.status).toBe(200); + expect(agent.recall).toHaveBeenCalled(); + }); +}); + +describe("recallEndpointInsight", () => { + it("should return undefined when no memories exist", async () => { + const agent = createMockAgent(); + agent.recall.mockResolvedValue([]); + + const insight = await recallEndpointInsight(agent, "https://api.example.com/paid"); + + expect(insight).toBeUndefined(); + }); + + it("should build insight from memories with cost info", async () => { + const agent = createMockAgent(); + agent.recall.mockResolvedValue([ + { content: "x402 payment to endpoint: success, cost: $0.03" }, + { content: "x402 payment to endpoint: success, cost: $0.05" }, + { content: "x402 payment to endpoint: failure, cost: $0.04" }, + ]); + + const insight = await recallEndpointInsight(agent, "https://api.example.com/paid"); + + expect(insight).toBeDefined(); + expect(insight!.interactionCount).toBe(3); + expect(insight!.averageCost).toBeCloseTo(0.04, 2); + expect(insight!.successRate).toBeCloseTo(2 / 3, 2); + }); + + it("should handle recall errors gracefully", async () => { + const agent = createMockAgent(); + agent.recall.mockRejectedValue(new Error("DB error")); + + const insight = await recallEndpointInsight(agent, "https://api.example.com/paid"); + + expect(insight).toBeUndefined(); + }); +}); + +describe("rememberPaymentOutcome", () => { + it("should store success memory and settle transaction", async () => { + const agent = createMockAgent(); + const result: MnemoPayPaymentResult = { + success: true, + transactionId: "tx-456", + endpoint: "https://api.example.com/paid", + cost: 0.05, + }; + + await rememberPaymentOutcome(agent, result); + + expect(agent.remember).toHaveBeenCalledWith( + "x402 payment to https://api.example.com/paid: success, cost: $0.05", + expect.objectContaining({ + importance: 0.7, + tags: ["x402", "success", "cost-tracked"], + }), + ); + expect(agent.settle).toHaveBeenCalledWith("tx-456"); + }); + + it("should store failure memory and refund transaction", async () => { + const agent = createMockAgent(); + const result: MnemoPayPaymentResult = { + success: false, + transactionId: "tx-789", + endpoint: "https://api.example.com/paid", + }; + + await rememberPaymentOutcome(agent, result); + + expect(agent.remember).toHaveBeenCalledWith( + "x402 payment to https://api.example.com/paid: failure", + expect.objectContaining({ + importance: 0.9, + tags: ["x402", "failure"], + }), + ); + expect(agent.refund).toHaveBeenCalledWith("tx-789"); + }); + + it("should not call settle/refund when there is no transactionId", async () => { + const agent = createMockAgent(); + const result: MnemoPayPaymentResult = { + success: true, + endpoint: "https://api.example.com/free", + }; + + await rememberPaymentOutcome(agent, result); + + expect(agent.settle).not.toHaveBeenCalled(); + expect(agent.refund).not.toHaveBeenCalled(); + }); +}); diff --git a/typescript/packages/http/mnemopay/src/middleware.ts b/typescript/packages/http/mnemopay/src/middleware.ts new file mode 100644 index 0000000000..93514db496 --- /dev/null +++ b/typescript/packages/http/mnemopay/src/middleware.ts @@ -0,0 +1,303 @@ +import type { + MnemoPayAgent, + MnemoPayMiddlewareConfig, + EndpointInsight, + MnemoPayMemory, + MnemoPayPaymentResult, +} from "./types"; + +const DEFAULT_RECALL_LIMIT = 5; +const DEFAULT_RELIABILITY_THRESHOLD = 0.3; + +/** + * Extract the endpoint URL from a fetch Request or URL input. + * + * @param input - The fetch input (string URL, URL object, or Request). + * @returns The URL string for the endpoint. + */ +function extractEndpoint(input: RequestInfo | URL): string { + if (typeof input === "string") return input; + if (input instanceof URL) return input.toString(); + return input.url; +} + +/** + * Try to extract a cost value from the 402 response body. + * + * Walks common x402 payment-required shapes to find a numeric cost. + * + * @param body - The parsed JSON body from the 402 response. + * @param customExtractor - Optional user-supplied cost extractor. + * @returns The cost as a number, or undefined. + */ +function extractCostFromPaymentRequired( + body: unknown, + customExtractor?: (paymentRequired: unknown) => number | undefined, +): number | undefined { + if (customExtractor) { + return customExtractor(body); + } + + if (!body || typeof body !== "object") return undefined; + + const record = body as Record; + + // v2: accepts[].maxAmountRequired + if (Array.isArray(record.accepts)) { + for (const accept of record.accepts) { + if (accept && typeof accept === "object" && "maxAmountRequired" in accept) { + const val = Number(accept.maxAmountRequired); + if (!isNaN(val)) return val; + } + } + } + + // v1: maxAmountRequired at top level + if ("maxAmountRequired" in record) { + const val = Number(record.maxAmountRequired); + if (!isNaN(val)) return val; + } + + return undefined; +} + +/** + * Build an EndpointInsight from recalled memories about a specific endpoint. + * + * @param endpoint - The URL of the endpoint. + * @param memories - The recalled memories to analyze. + * @returns An EndpointInsight summarizing the agent's experience with this endpoint. + */ +function buildInsight(endpoint: string, memories: MnemoPayMemory[]): EndpointInsight { + const costs: number[] = []; + let successes = 0; + let failures = 0; + + for (const mem of memories) { + const costMatch = mem.content.match(/cost[:\s]+\$?([\d.]+)/i); + if (costMatch) { + const c = parseFloat(costMatch[1]); + if (!isNaN(c)) costs.push(c); + } + if (/success|settled|completed/i.test(mem.content)) successes++; + if (/fail|refund|error/i.test(mem.content)) failures++; + } + + const total = successes + failures; + + return { + endpoint, + averageCost: costs.length > 0 ? costs.reduce((a, b) => a + b, 0) / costs.length : undefined, + successRate: total > 0 ? successes / total : undefined, + interactionCount: memories.length, + memories, + }; +} + +/** + * Recall what the agent knows about a given endpoint before making a payment. + * + * This is the "look before you leap" step — the agent checks its memory for + * past interactions with this endpoint, including cost history, reliability, + * and any alternative endpoints it has learned about. + * + * @param agent - The MnemoPay agent instance. + * @param endpoint - The URL being requested. + * @param recallLimit - Maximum number of memories to recall. + * @returns An EndpointInsight, or undefined if the agent has no memories. + */ +export async function recallEndpointInsight( + agent: MnemoPayAgent, + endpoint: string, + recallLimit: number = DEFAULT_RECALL_LIMIT, +): Promise { + try { + const memories = await agent.recall(`x402 payment to ${endpoint}`, recallLimit); + if (!memories || memories.length === 0) return undefined; + return buildInsight(endpoint, memories); + } catch { + // Recall failures should not block the payment flow + return undefined; + } +} + +/** + * Record the outcome of a payment in the agent's memory. + * + * After a 402 payment completes (successfully or not), this function stores + * a rich memory entry that the agent can use to make better decisions in the + * future — choosing cheaper endpoints, avoiding unreliable ones, or + * negotiating better terms. + * + * @param agent - The MnemoPay agent instance. + * @param result - The payment result to remember. + * @param debug - Whether to log to console. + * @returns A promise that resolves when the memory is stored. + */ +export async function rememberPaymentOutcome( + agent: MnemoPayAgent, + result: MnemoPayPaymentResult, + debug: boolean = false, +): Promise { + const costStr = result.cost !== undefined ? `, cost: $${result.cost}` : ""; + const status = result.success ? "success" : "failure"; + const content = `x402 payment to ${result.endpoint}: ${status}${costStr}`; + + const tags = ["x402", status]; + if (result.cost !== undefined) tags.push("cost-tracked"); + + const importance = result.success ? 0.7 : 0.9; // failures are more important to remember + + try { + await agent.remember(content, { importance, tags }); + + if (result.success && result.transactionId) { + await agent.settle(result.transactionId); + if (debug) console.log(`[mnemopay] settled tx ${result.transactionId} for ${result.endpoint}`); + } else if (!result.success && result.transactionId) { + await agent.refund(result.transactionId); + if (debug) console.log(`[mnemopay] refunded tx ${result.transactionId} for ${result.endpoint}`); + } + } catch { + // Memory storage failures should not disrupt the payment flow + if (debug) console.log(`[mnemopay] failed to store memory for ${result.endpoint}`); + } +} + +/** + * Wraps a fetch function with MnemoPay memory-enhanced x402 payment handling. + * + * This is the primary integration point. It intercepts every fetch call and: + * 1. Before the request: recalls past payment experiences with the endpoint + * 2. If a 402 is returned: records a charge via MnemoPay + * 3. After the retry succeeds: settles the transaction (reinforcing the memory) + * 4. If the retry fails: refunds the transaction (docking reputation) + * + * The wrapped fetch does NOT replace x402 payment logic — it layers memory on + * top of an already-x402-wrapped fetch function. Use it like: + * + * ```typescript + * const payFetch = wrapFetchWithPayment(fetch, client); // x402 layer + * const smartFetch = withMnemoPay(payFetch, { agent: myAgent }); // memory layer + * ``` + * + * @param fetchFn - A fetch function (ideally already wrapped with x402 payment handling). + * @param config - MnemoPay middleware configuration. + * @returns A wrapped fetch function with memory-enhanced payment awareness. + */ +export function withMnemoPay( + fetchFn: typeof globalThis.fetch, + config: MnemoPayMiddlewareConfig, +): typeof globalThis.fetch { + const { agent, recallLimit = DEFAULT_RECALL_LIMIT, debug = false, extractCost } = config; + const reliabilityThreshold = + config.reliabilityThreshold ?? DEFAULT_RELIABILITY_THRESHOLD; + + return async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const endpoint = extractEndpoint(input); + + // --- Phase 1: Recall past experience --- + const insight = await recallEndpointInsight(agent, endpoint, recallLimit); + + if (debug && insight) { + console.log( + `[mnemopay] recalled ${insight.interactionCount} memories for ${endpoint}` + + (insight.successRate !== undefined ? `, success rate: ${(insight.successRate * 100).toFixed(0)}%` : "") + + (insight.averageCost !== undefined ? `, avg cost: $${insight.averageCost.toFixed(4)}` : ""), + ); + } + + // Warn if we know this endpoint is unreliable + if ( + insight && + insight.successRate !== undefined && + insight.successRate < reliabilityThreshold && + insight.interactionCount >= 3 + ) { + if (debug) { + console.log( + `[mnemopay] WARNING: ${endpoint} has low reliability (${(insight.successRate * 100).toFixed(0)}%). Consider alternatives.`, + ); + } + } + + // --- Phase 2: Make the request (x402 payment happens inside fetchFn) --- + let response: Response; + try { + response = await fetchFn(input, init); + } catch (error) { + // The request itself failed (network error, x402 payment creation failed, etc.) + await rememberPaymentOutcome( + agent, + { + success: false, + endpoint, + priorInsights: insight ?? undefined, + }, + debug, + ); + throw error; + } + + // --- Phase 3: Analyze the outcome --- + // If the response is a 402, the upstream x402 wrapper already tried to pay + // and the retry still failed — this is a payment failure. + const success = response.status !== 402 && response.ok; + + // Try to extract cost from payment response headers + let cost: number | undefined; + const paymentResponseHeader = + response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE"); + if (paymentResponseHeader) { + try { + const parsed = JSON.parse(atob(paymentResponseHeader)); + if (parsed && typeof parsed === "object" && "amount" in parsed) { + cost = Number(parsed.amount); + } + } catch { + // Not all payment response headers are base64 JSON; that's fine + } + } + + // Record a charge in MnemoPay if we detected a payment was made + let transactionId: string | undefined; + if (cost !== undefined) { + try { + transactionId = await agent.charge(cost, `x402 payment to ${endpoint}`); + } catch { + if (debug) console.log(`[mnemopay] failed to record charge for ${endpoint}`); + } + } + + // --- Phase 4: Remember the outcome --- + await rememberPaymentOutcome( + agent, + { + success, + transactionId, + endpoint, + cost, + priorInsights: insight ?? undefined, + }, + debug, + ); + + return response; + }; +} + +/** + * Creates a MnemoPay-enhanced fetch function with default configuration. + * + * Convenience wrapper around `withMnemoPay` for quick setup. + * + * @param fetchFn - A fetch function (ideally already wrapped with x402 payment handling). + * @param agent - The MnemoPay agent instance. + * @returns A wrapped fetch function with memory-enhanced payment awareness. + */ +export function withMnemoPayAgent( + fetchFn: typeof globalThis.fetch, + agent: MnemoPayAgent, +): typeof globalThis.fetch { + return withMnemoPay(fetchFn, { agent }); +} diff --git a/typescript/packages/http/mnemopay/src/types.ts b/typescript/packages/http/mnemopay/src/types.ts new file mode 100644 index 0000000000..f2832aca5a --- /dev/null +++ b/typescript/packages/http/mnemopay/src/types.ts @@ -0,0 +1,155 @@ +/** + * MnemoPay integration types for x402. + * + * These types define the interface contract between the x402 payment protocol + * and the MnemoPay SDK, allowing this package to work with any object that + * implements the MnemoPayAgent interface — whether that's MnemoPayLite, + * MnemoPayFull, or a custom implementation. + */ + +/** + * Represents a memory entry recalled from the MnemoPay agent. + */ +export interface MnemoPayMemory { + /** The content of the stored memory. */ + content: string; + /** Relevance score from 0 to 1. */ + relevance?: number; + /** Tags associated with this memory. */ + tags?: string[]; + /** Additional metadata attached to the memory. */ + metadata?: Record; +} + +/** + * The minimal interface a MnemoPay agent must implement for x402 integration. + * + * This mirrors the core API of @mnemopay/sdk's MnemoPayLite class, allowing + * the middleware to store payment outcomes, recall past experiences, and + * update reputation based on settlement results. + */ +export interface MnemoPayAgent { + /** + * Store a memory about a payment event. + * + * @param content - Human-readable description of what happened. + * @param options - Optional metadata including importance and tags. + * @returns A promise that resolves when the memory is stored. + */ + remember( + content: string, + options?: { importance?: number; tags?: string[] }, + ): Promise; + + /** + * Recall memories relevant to a query. + * + * @param query - Natural-language query to search memories. + * @param limit - Maximum number of memories to return. + * @returns A promise resolving to an array of matching memories. + */ + recall(query: string, limit?: number): Promise; + + /** + * Record a charge/payment and get a transaction ID. + * + * @param amount - The payment amount. + * @param description - Human-readable description of the charge. + * @returns A promise resolving to a transaction identifier. + */ + charge(amount: number, description: string): Promise; + + /** + * Settle a transaction — reinforces the positive memory. + * + * @param transactionId - The transaction to settle. + * @returns A promise that resolves when settlement is recorded. + */ + settle(transactionId: string): Promise; + + /** + * Refund a transaction — docks reputation and weakens the memory. + * + * @param transactionId - The transaction to refund. + * @returns A promise that resolves when refund is recorded. + */ + refund(transactionId: string): Promise; + + /** + * Get the agent's current balance and reputation. + * + * @returns An object with wallet balance and reputation score. + */ + balance(): { wallet: number; reputation: number }; +} + +/** + * Configuration options for the MnemoPay x402 middleware. + */ +export interface MnemoPayMiddlewareConfig { + /** The MnemoPay agent instance. */ + agent: MnemoPayAgent; + + /** + * Number of past memories to recall before each request. + * + * @defaultValue 5 + */ + recallLimit?: number; + + /** + * Minimum reputation score of a remembered endpoint to consider it reliable. + * Endpoints below this threshold trigger a warning in recalled memories. + * + * @defaultValue 0.3 + */ + reliabilityThreshold?: number; + + /** + * Whether to log memory events to the console for debugging. + * + * @defaultValue false + */ + debug?: boolean; + + /** + * Custom function to extract a cost amount (as a number) from payment requirements. + * Defaults to parsing the `maxAmountRequired` field. + * + * @param paymentRequired - The raw payment requirements object. + * @returns The cost as a number, or undefined if it cannot be determined. + */ + extractCost?: (paymentRequired: unknown) => number | undefined; +} + +/** + * Information about an endpoint's payment history, derived from MnemoPay memories. + */ +export interface EndpointInsight { + /** The endpoint URL. */ + endpoint: string; + /** Average cost observed across remembered payments. */ + averageCost?: number; + /** Success rate from 0 to 1 based on remembered outcomes. */ + successRate?: number; + /** Number of remembered interactions with this endpoint. */ + interactionCount: number; + /** Raw memories about this endpoint. */ + memories: MnemoPayMemory[]; +} + +/** + * Result returned after a MnemoPay-enhanced payment flow completes. + */ +export interface MnemoPayPaymentResult { + /** Whether the payment succeeded. */ + success: boolean; + /** The MnemoPay transaction ID, if a charge was recorded. */ + transactionId?: string; + /** The endpoint that was paid. */ + endpoint: string; + /** The cost that was paid, if determinable. */ + cost?: number; + /** Insights recalled about this endpoint before the payment. */ + priorInsights?: EndpointInsight; +} diff --git a/typescript/packages/http/mnemopay/tsconfig.json b/typescript/packages/http/mnemopay/tsconfig.json new file mode 100644 index 0000000000..1177655907 --- /dev/null +++ b/typescript/packages/http/mnemopay/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false, + "lib": ["DOM"] + }, + "include": ["src"] +} diff --git a/typescript/packages/http/mnemopay/tsup.config.ts b/typescript/packages/http/mnemopay/tsup.config.ts new file mode 100644 index 0000000000..f8699f925c --- /dev/null +++ b/typescript/packages/http/mnemopay/tsup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "node16", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/http/mnemopay/vitest.config.ts b/typescript/packages/http/mnemopay/vitest.config.ts new file mode 100644 index 0000000000..156f8c924f --- /dev/null +++ b/typescript/packages/http/mnemopay/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +}));