@elizaos/plugin-x402 2.0.0-beta.1 → 2.0.3-beta.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"payment-wrapper.d.ts","sourceRoot":"","sources":["../src/payment-wrapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAET,mBAAmB,EACnB,KAAK,EAIN,MAAM,eAAe,CAAC;AAsEvB;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAEtC,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM7D;AAquCD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,mBAAmB,GACzB,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CA0Q/B;AAqTD,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEhF;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD,KAAK,EAAE,CAkCT"}
1
+ {"version":3,"file":"payment-wrapper.d.ts","sourceRoot":"","sources":["../src/payment-wrapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAET,mBAAmB,EACnB,KAAK,EAIN,MAAM,eAAe,CAAC;AAsEvB;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAEtC,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM7D;AAquCD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,mBAAmB,GACzB,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CA4Q/B;AAqTD,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEhF;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD,KAAK,EAAE,CAsCT"}
@@ -1 +1 @@
1
- {"version":3,"file":"startup-validator.d.ts","sourceRoot":"","sources":["../src/startup-validator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,SAAS,EAGT,KAAK,EACN,MAAM,eAAe,CAAC;AAUvB;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAiQD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,KAAK,EAAE,EACf,SAAS,CAAC,EAAE,SAAS,EACrB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,uBAAuB,CAoCzB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,KAAK,EAAE,EACf,SAAS,CAAC,EAAE,SAAS,EACrB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,IAAI,CAUN"}
1
+ {"version":3,"file":"startup-validator.d.ts","sourceRoot":"","sources":["../src/startup-validator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,SAAS,EAGT,KAAK,EACN,MAAM,eAAe,CAAC;AAUvB;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAmQD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,KAAK,EAAE,EACf,SAAS,CAAC,EAAE,SAAS,EACrB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,uBAAuB,CAoCzB;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,KAAK,EAAE,EACf,SAAS,CAAC,EAAE,SAAS,EACrB,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,IAAI,CAUN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elizaos/plugin-x402",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.3-beta.5",
4
4
  "description": "x402 micropayment middleware for elizaOS plugin HTTP routes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,8 +17,24 @@
17
17
  "./package.json": "./package.json",
18
18
  ".": {
19
19
  "types": "./dist/index.d.ts",
20
+ "eliza-source": {
21
+ "types": "./src/index.ts",
22
+ "import": "./src/index.ts",
23
+ "default": "./src/index.ts"
24
+ },
20
25
  "import": "./dist/index.js",
21
26
  "default": "./dist/index.js"
27
+ },
28
+ "./*.css": "./dist/*.css",
29
+ "./*": {
30
+ "types": "./dist/*.d.ts",
31
+ "eliza-source": {
32
+ "types": "./src/*.ts",
33
+ "import": "./src/*.ts",
34
+ "default": "./src/*.ts"
35
+ },
36
+ "import": "./dist/*.js",
37
+ "default": "./dist/*.js"
22
38
  }
23
39
  },
24
40
  "files": [
@@ -44,22 +60,25 @@
44
60
  "scripts": {
45
61
  "build": "bun run build.ts",
46
62
  "build:ts": "bun run build.ts",
47
- "clean": "rm -rf dist",
48
- "typecheck": "tsc --noEmit",
49
- "lint": "echo \"Lint skipped\"",
50
- "lint:check": "bun run lint"
63
+ "clean": "node ../../packages/scripts/rm-path-recursive.mjs dist",
64
+ "test": "vitest run",
65
+ "typecheck": "tsgo --noEmit",
66
+ "lint": "bun run typecheck",
67
+ "lint:check": "bun run typecheck"
51
68
  },
52
69
  "dependencies": {
53
- "@elizaos/core": "2.0.0-beta.1",
70
+ "@elizaos/core": "2.0.3-beta.5",
71
+ "@solana/web3.js": "1.98.4",
54
72
  "drizzle-orm": "0.45.2",
55
73
  "viem": "^2.48.8"
56
74
  },
57
75
  "devDependencies": {
58
76
  "@types/node": "^24.0.0",
59
- "typescript": "^6.0.3"
77
+ "typescript": "^6.0.3",
78
+ "vitest": "^4.0.18"
60
79
  },
61
80
  "peerDependencies": {
62
- "@elizaos/core": "2.0.0-beta.1"
81
+ "@elizaos/core": "2.0.3-beta.5"
63
82
  },
64
83
  "publishConfig": {
65
84
  "access": "public"
@@ -67,5 +86,6 @@
67
86
  "agentConfig": {
68
87
  "pluginType": "elizaos:plugin:1.0.0",
69
88
  "pluginParameters": {}
70
- }
89
+ },
90
+ "gitHead": "ff6157011c9459670021cc28a6797592a78b8817"
71
91
  }
@@ -0,0 +1,10 @@
1
+ import { vi } from "vitest";
2
+
3
+ vi.mock("@elizaos/core", () => ({
4
+ logger: {
5
+ debug: vi.fn(),
6
+ error: vi.fn(),
7
+ info: vi.fn(),
8
+ warn: vi.fn(),
9
+ },
10
+ }));
package/src/index.ts CHANGED
@@ -66,16 +66,16 @@ export {
66
66
  isRoutePaymentWrapped,
67
67
  X402_ROUTE_PAYMENT_WRAPPED,
68
68
  } from "./payment-wrapper.js";
69
- export {
70
- resolveEffectiveX402,
71
- X402_EVENT_PAYMENT_REQUIRED,
72
- X402_EVENT_PAYMENT_VERIFIED,
73
- } from "./x402-resolve.js";
74
69
  export {
75
70
  type StartupValidationResult,
76
71
  validateAndThrowIfInvalid,
77
72
  validateX402Startup,
78
73
  } from "./startup-validator.js";
74
+ export {
75
+ resolveEffectiveX402,
76
+ X402_EVENT_PAYMENT_REQUIRED,
77
+ X402_EVENT_PAYMENT_VERIFIED,
78
+ } from "./x402-resolve.js";
79
79
 
80
80
  export {
81
81
  type Accepts,
@@ -107,6 +107,8 @@ const x402Plugin: Plugin = {
107
107
  providers: [],
108
108
  evaluators: [],
109
109
  services: [],
110
+ // Middleware-only plugin — no service instances or persistent resources to dispose.
111
+ dispose: async (_runtime) => {},
110
112
  };
111
113
 
112
114
  export default x402Plugin;
@@ -0,0 +1,234 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ applyPaymentProtection,
4
+ isRoutePaymentWrapped,
5
+ } from "./payment-wrapper.js";
6
+
7
+ function makeResponse() {
8
+ const res = {
9
+ headersSent: false,
10
+ statusCode: 200,
11
+ headers: new Map<string, string>(),
12
+ setHeader: vi.fn((name: string, value: string) => {
13
+ res.headers.set(name, value);
14
+ return res;
15
+ }),
16
+ status: vi.fn((code: number) => {
17
+ res.statusCode = code;
18
+ return res;
19
+ }),
20
+ json: vi.fn((body: unknown) => body),
21
+ };
22
+ return res;
23
+ }
24
+
25
+ function decodePaymentRequiredHeader(res: ReturnType<typeof makeResponse>) {
26
+ const encoded = res.headers.get("PAYMENT-REQUIRED");
27
+ expect(encoded).toEqual(expect.any(String));
28
+ return JSON.parse(Buffer.from(String(encoded), "base64").toString("utf8")) as
29
+ | Record<string, unknown>
30
+ | undefined;
31
+ }
32
+
33
+ describe("applyPaymentProtection", () => {
34
+ afterEach(() => {
35
+ vi.restoreAllMocks();
36
+ vi.unstubAllGlobals();
37
+ });
38
+
39
+ it("rejects non-array route input", () => {
40
+ expect(() => applyPaymentProtection({} as never)).toThrow(
41
+ "routes must be an array",
42
+ );
43
+ });
44
+
45
+ it("leaves unprotected routes unchanged", () => {
46
+ const route = { path: "/free", type: "GET", handler: vi.fn() } as never;
47
+
48
+ const [result] = applyPaymentProtection([route]);
49
+
50
+ expect(result).toBe(route);
51
+ expect(isRoutePaymentWrapped(result)).toBe(false);
52
+ });
53
+
54
+ it("wraps protected routes and returns payment-required responses", async () => {
55
+ const handler = vi.fn();
56
+ const route = {
57
+ path: "/paid",
58
+ type: "GET",
59
+ handler,
60
+ x402: { priceInCents: 25, paymentConfigs: ["base_usdc"] },
61
+ } as never;
62
+ const runtime = { agentId: "agent-1", emitEvent: vi.fn() };
63
+ const res = makeResponse();
64
+
65
+ const [wrapped] = applyPaymentProtection([route], {
66
+ agentId: "agent-1",
67
+ });
68
+
69
+ expect(wrapped).not.toBe(route);
70
+ expect(isRoutePaymentWrapped(wrapped)).toBe(true);
71
+
72
+ await wrapped.handler?.(
73
+ { method: "GET", headers: {}, query: {} } as never,
74
+ res as never,
75
+ runtime as never,
76
+ );
77
+
78
+ expect(handler).not.toHaveBeenCalled();
79
+ expect(res.status).toHaveBeenCalledWith(402);
80
+ expect(res.json).toHaveBeenCalledWith(
81
+ expect.objectContaining({ x402Version: 1 }),
82
+ );
83
+ expect(runtime.emitEvent).toHaveBeenCalledWith(
84
+ "PAYMENT_REQUIRED",
85
+ expect.objectContaining({
86
+ path: "/paid",
87
+ reason: "payment_required",
88
+ }),
89
+ );
90
+ expect(res.headers.get("Access-Control-Expose-Headers")).toContain(
91
+ "PAYMENT-REQUIRED",
92
+ );
93
+ expect(decodePaymentRequiredHeader(res)).toEqual(
94
+ expect.objectContaining({
95
+ error: "Payment Required",
96
+ }),
97
+ );
98
+ });
99
+
100
+ it("handles route requests with missing optional headers and query", async () => {
101
+ const handler = vi.fn();
102
+ const route = {
103
+ path: "/paid",
104
+ type: "POST",
105
+ handler,
106
+ x402: { priceInCents: 10, paymentConfigs: ["base_usdc"] },
107
+ } as never;
108
+ const runtime = { agentId: "agent-1", emitEvent: vi.fn() };
109
+ const res = makeResponse();
110
+ const [wrapped] = applyPaymentProtection([route], {
111
+ agentId: "agent-1",
112
+ });
113
+
114
+ await wrapped.handler?.({ method: "POST" } as never, res as never, runtime as never);
115
+
116
+ expect(handler).not.toHaveBeenCalled();
117
+ expect(res.status).toHaveBeenCalledWith(402);
118
+ expect(runtime.emitEvent).toHaveBeenCalledWith(
119
+ "PAYMENT_REQUIRED",
120
+ expect.objectContaining({ reason: "payment_required" }),
121
+ );
122
+ });
123
+
124
+ it("returns payment-required responses when validators fail before proof checks", async () => {
125
+ const handler = vi.fn();
126
+ const route = {
127
+ path: "/paid",
128
+ type: "GET",
129
+ handler,
130
+ validator: vi.fn(async () => ({
131
+ valid: false,
132
+ error: {
133
+ message: "bad request",
134
+ details: { field: "amount" },
135
+ },
136
+ })),
137
+ x402: { priceInCents: 10, paymentConfigs: ["base_usdc"] },
138
+ } as never;
139
+ const runtime = { agentId: "agent-1", emitEvent: vi.fn() };
140
+ const res = makeResponse();
141
+ const [wrapped] = applyPaymentProtection([route], {
142
+ agentId: "agent-1",
143
+ });
144
+
145
+ await wrapped.handler?.(
146
+ {
147
+ method: "GET",
148
+ headers: { "x-payment-id": "valid-looking-id" },
149
+ query: {},
150
+ } as never,
151
+ res as never,
152
+ runtime as never,
153
+ );
154
+
155
+ expect(handler).not.toHaveBeenCalled();
156
+ expect(route.validator).toHaveBeenCalledTimes(1);
157
+ expect(res.status).toHaveBeenCalledWith(402);
158
+ expect(res.json).toHaveBeenCalledWith(
159
+ expect.objectContaining({
160
+ error: 'bad request: {"field":"amount"}',
161
+ }),
162
+ );
163
+ expect(runtime.emitEvent).toHaveBeenCalledWith(
164
+ "PAYMENT_REQUIRED",
165
+ expect.objectContaining({ reason: "validator_failed" }),
166
+ );
167
+ expect(decodePaymentRequiredHeader(res)).toEqual(
168
+ expect.objectContaining({
169
+ error: 'bad request: {"field":"amount"}',
170
+ }),
171
+ );
172
+ });
173
+
174
+ it("rejects hostile payment ids before facilitator fetch", async () => {
175
+ const handler = vi.fn();
176
+ const fetchMock = vi.fn();
177
+ vi.stubGlobal("fetch", fetchMock);
178
+ const route = {
179
+ path: "/paid",
180
+ type: "GET",
181
+ handler,
182
+ x402: { priceInCents: 10, paymentConfigs: ["base_usdc"] },
183
+ } as never;
184
+ const runtime = {
185
+ agentId: "agent-1",
186
+ emitEvent: vi.fn(),
187
+ getSetting: vi.fn(() => "https://facilitator.test"),
188
+ };
189
+ const res = makeResponse();
190
+ const [wrapped] = applyPaymentProtection([route], {
191
+ agentId: "agent-1",
192
+ });
193
+
194
+ await wrapped.handler?.(
195
+ {
196
+ method: "GET",
197
+ headers: { "x-payment-id": "../paid\n" },
198
+ query: {},
199
+ } as never,
200
+ res as never,
201
+ runtime as never,
202
+ );
203
+
204
+ expect(fetchMock).not.toHaveBeenCalled();
205
+ expect(handler).not.toHaveBeenCalled();
206
+ expect(res.status).toHaveBeenCalledWith(402);
207
+ expect(res.json).toHaveBeenCalledWith(
208
+ expect.objectContaining({
209
+ error: "Payment verification failed",
210
+ }),
211
+ );
212
+ expect(runtime.emitEvent).toHaveBeenCalledWith(
213
+ "PAYMENT_REQUIRED",
214
+ expect.objectContaining({ reason: "verification_failed" }),
215
+ );
216
+ });
217
+
218
+ it("does not wrap routes that are already marked as wrapped", () => {
219
+ const route = {
220
+ path: "/paid",
221
+ type: "GET",
222
+ handler: vi.fn(),
223
+ x402: { priceInCents: 25, paymentConfigs: ["base_usdc"] },
224
+ } as never;
225
+
226
+ const [wrapped] = applyPaymentProtection([route]);
227
+ const firstHandler = wrapped.handler;
228
+ const [again] = applyPaymentProtection([wrapped]);
229
+
230
+ expect(again).toBe(wrapped);
231
+ expect(again.handler).toBe(firstHandler);
232
+ expect(isRoutePaymentWrapped(again)).toBe(true);
233
+ });
234
+ });
@@ -1473,24 +1473,26 @@ export function createPaymentAwareHandler(
1473
1473
  }
1474
1474
  }
1475
1475
 
1476
- log("Headers:", JSON.stringify(typedReq.headers, null, 2));
1477
- log("Query:", JSON.stringify(typedReq.query, null, 2));
1476
+ const requestHeaders = typedReq.headers ?? {};
1477
+ const requestQuery = typedReq.query ?? {};
1478
+
1479
+ log("Headers:", JSON.stringify(requestHeaders, null, 2));
1480
+ log("Query:", JSON.stringify(requestQuery, null, 2));
1478
1481
  if (typedReq.method === "POST" && typedReq.body) {
1479
1482
  log("Body:", JSON.stringify(typedReq.body, null, 2));
1480
1483
  }
1481
1484
 
1482
1485
  const paymentProof =
1483
- typedReq.headers["x-payment-proof"] ||
1484
- typedReq.headers["x-payment"] ||
1485
- typedReq.headers["payment-signature"] ||
1486
- typedReq.query?.paymentProof;
1487
- const paymentId =
1488
- typedReq.headers["x-payment-id"] || typedReq.query?.paymentId;
1486
+ requestHeaders["x-payment-proof"] ||
1487
+ requestHeaders["x-payment"] ||
1488
+ requestHeaders["payment-signature"] ||
1489
+ requestQuery.paymentProof;
1490
+ const paymentId = requestHeaders["x-payment-id"] || requestQuery.paymentId;
1489
1491
 
1490
1492
  log("Payment credentials:", {
1491
- "x-payment-proof": !!typedReq.headers["x-payment-proof"],
1492
- "x-payment": !!typedReq.headers["x-payment"],
1493
- "payment-signature": !!typedReq.headers["payment-signature"],
1493
+ "x-payment-proof": !!requestHeaders["x-payment-proof"],
1494
+ "x-payment": !!requestHeaders["x-payment"],
1495
+ "payment-signature": !!requestHeaders["payment-signature"],
1494
1496
  "x-payment-id": !!paymentId,
1495
1497
  found: !!(paymentProof || paymentId),
1496
1498
  });
@@ -1974,6 +1976,10 @@ export function applyPaymentProtection(
1974
1976
  return routes.map((route) => {
1975
1977
  const x402Route = route as PaymentEnabledRoute;
1976
1978
  if (x402Route.x402 != null) {
1979
+ if (isRoutePaymentWrapped(route)) {
1980
+ return route;
1981
+ }
1982
+
1977
1983
  logger.debug(
1978
1984
  { path: x402Route.path, x402: x402Route.x402 },
1979
1985
  "[x402] payment protection enabled",
@@ -0,0 +1,86 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { validateX402Startup } from "./startup-validator.js";
3
+
4
+ const originalEnv = { ...process.env };
5
+
6
+ function paidRoute(x402: unknown, overrides: Record<string, unknown> = {}) {
7
+ return {
8
+ path: "/paid",
9
+ type: "GET",
10
+ handler: vi.fn(),
11
+ x402,
12
+ ...overrides,
13
+ } as never;
14
+ }
15
+
16
+ describe("validateX402Startup", () => {
17
+ beforeEach(() => {
18
+ process.env = { ...originalEnv, NODE_ENV: "test" };
19
+ });
20
+
21
+ afterEach(() => {
22
+ process.env = { ...originalEnv };
23
+ });
24
+
25
+ it("rejects x402 true when character defaults are missing", () => {
26
+ const result = validateX402Startup([paidRoute(true)]);
27
+
28
+ expect(result.valid).toBe(false);
29
+ expect(result.errors).toEqual(
30
+ expect.arrayContaining([
31
+ expect.stringContaining("defaultPriceInCents"),
32
+ expect.stringContaining("defaultPaymentConfigs"),
33
+ ]),
34
+ );
35
+ });
36
+
37
+ it("uses character defaults for partial route configuration", () => {
38
+ const result = validateX402Startup(
39
+ [paidRoute({ priceInCents: 25 })],
40
+ {
41
+ settings: {
42
+ x402: {
43
+ defaultPriceInCents: 10,
44
+ defaultPaymentConfigs: ["base_usdc"],
45
+ },
46
+ },
47
+ } as never,
48
+ );
49
+
50
+ expect(result.valid).toBe(true);
51
+ expect(result.errors).toEqual([]);
52
+ });
53
+
54
+ it("rejects unknown payment config names", () => {
55
+ const result = validateX402Startup([
56
+ paidRoute({ priceInCents: 25, paymentConfigs: ["missing_config"] }),
57
+ ]);
58
+
59
+ expect(result.valid).toBe(false);
60
+ expect(result.errors).toEqual([
61
+ expect.stringContaining("unknown payment config 'missing_config'"),
62
+ ]);
63
+ });
64
+
65
+ it("rejects primitive x402 values", () => {
66
+ const result = validateX402Startup([paidRoute("yes")]);
67
+
68
+ expect(result.valid).toBe(false);
69
+ expect(result.errors).toEqual([
70
+ expect.stringContaining("x402 must be true or a configuration object"),
71
+ ]);
72
+ });
73
+
74
+ it("rejects protected routes without handlers", () => {
75
+ const result = validateX402Startup([
76
+ paidRoute({ priceInCents: 25, paymentConfigs: ["base_usdc"] }, {
77
+ handler: undefined,
78
+ }),
79
+ ]);
80
+
81
+ expect(result.valid).toBe(false);
82
+ expect(result.errors).toEqual([
83
+ expect.stringContaining("route has x402 protection but no handler"),
84
+ ]);
85
+ });
86
+ });
@@ -175,7 +175,7 @@ function validateX402Route(
175
175
  `${routePath}: x402: true requires character.settings.x402.defaultPaymentConfigs (non-empty array)`,
176
176
  );
177
177
  }
178
- } else if (typeof raw === "object") {
178
+ } else if (typeof raw === "object" && !Array.isArray(raw)) {
179
179
  priceInCents = raw.priceInCents ?? cx?.defaultPriceInCents;
180
180
  paymentConfigs = (raw.paymentConfigs ?? cx?.defaultPaymentConfigs) as
181
181
  | string[]
@@ -190,6 +190,8 @@ function validateX402Route(
190
190
  `${routePath}: x402.paymentConfigs is required (or set character.settings.x402.defaultPaymentConfigs)`,
191
191
  );
192
192
  }
193
+ } else {
194
+ errors.push(`${routePath}: x402 must be true or a configuration object`);
193
195
  }
194
196
 
195
197
  if (priceInCents !== undefined && priceInCents !== null) {
@@ -135,7 +135,7 @@ export async function replayGuardCommit(
135
135
  const exp = Date.now() + replayWindowMs();
136
136
  // Require all keys to map to the same owner. If owners diverge, the
137
137
  // in-process map raced with another request — drop the owner so the durable
138
- // layer can no-op the owner-bound path instead of recording wrong lineage.
138
+ // layer skips the owner-bound path instead of recording wrong lineage.
139
139
  let owner: string | undefined;
140
140
  let ownerConsistent = true;
141
141
  for (const k of keys) {