@agentcash/telemetry 0.3.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,128 @@
1
+ # @merit-systems/x402-server-telemetry
2
+
3
+ [![npm](https://img.shields.io/npm/v/@merit-systems/x402-server-telemetry)](https://www.npmjs.com/package/@merit-systems/x402-server-telemetry)
4
+
5
+ Shared telemetry for Merit Systems x402 servers. Extracts identity headers, logs invocations to ClickHouse, and auto-extracts verified wallets from x402 payments and SIWX auth.
6
+
7
+ [Telemetry spec](docs/telemetry-spec.md) | [npm](https://www.npmjs.com/package/@merit-systems/x402-server-telemetry) | [GitHub](https://github.com/Merit-Systems/x402-server-telemetry)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @merit-systems/x402-server-telemetry @clickhouse/client
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```typescript
18
+ // lib/telemetry.ts (or wherever your route wrappers live)
19
+ import { initTelemetry, withTelemetry } from '@merit-systems/x402-server-telemetry';
20
+
21
+ initTelemetry({
22
+ clickhouse: {
23
+ url: process.env.TELEM_CLICKHOUSE_URL!,
24
+ database: process.env.TELEM_CLICKHOUSE_DATABASE,
25
+ username: process.env.TELEM_CLICKHOUSE_USERNAME,
26
+ password: process.env.TELEM_CLICKHOUSE_PASSWORD,
27
+ },
28
+ verify: true, // optional — pings ClickHouse on startup, logs success/failure
29
+ });
30
+
31
+ export { withTelemetry };
32
+ ```
33
+
34
+ ```typescript
35
+ // app/api/example/route.ts
36
+ import { withTelemetry } from '@/lib/telemetry';
37
+
38
+ export const POST = withTelemetry(async (request, ctx) => {
39
+ return NextResponse.json(await doWork(request));
40
+ });
41
+ ```
42
+
43
+ ## Three entrypoints
44
+
45
+ ### Core (`@merit-systems/x402-server-telemetry`)
46
+
47
+ Requires: `@clickhouse/client`, `next`
48
+
49
+ ```typescript
50
+ import { initTelemetry, withTelemetry } from '@merit-systems/x402-server-telemetry';
51
+ ```
52
+
53
+ - `initTelemetry(config)` — synchronous, call once at module level. Pass `verify: true` to ping ClickHouse on startup (fire-and-forget, never blocks)
54
+ - `withTelemetry(handler)` — wrap any Next.js route handler
55
+ - `extractVerifiedWallet(headers)` — extract wallet from x402 payment headers
56
+
57
+ ### SIWX (`@merit-systems/x402-server-telemetry/siwx`)
58
+
59
+ Requires: `@x402/extensions`, `@x402/core`
60
+
61
+ ```typescript
62
+ import { withSiwxTelemetry } from '@merit-systems/x402-server-telemetry/siwx';
63
+
64
+ export const GET = withSiwxTelemetry(async (request, ctx) => {
65
+ // ctx.verifiedWallet is guaranteed to be set
66
+ return NextResponse.json(await getJobs(ctx.verifiedWallet));
67
+ });
68
+ ```
69
+
70
+ ### Route Builder (`@merit-systems/x402-server-telemetry/builder`)
71
+
72
+ Requires: `@x402/next`, `zod` (^4), `@x402/extensions`
73
+
74
+ ```typescript
75
+ import { createRouteBuilder } from '@merit-systems/x402-server-telemetry/builder';
76
+
77
+ const route = createRouteBuilder({ x402Server });
78
+
79
+ export const POST = route
80
+ .price('0.05', 'base:8453')
81
+ .body(searchSchema)
82
+ .handler(async ({ body }) => searchPeople(body.query));
83
+ ```
84
+
85
+ ## Next.js integration footguns
86
+
87
+ ### `@clickhouse/client` must be externalized
88
+
89
+ The ClickHouse client uses Node.js native APIs that break when bundled by Next.js. Add to your `next.config`:
90
+
91
+ ```typescript
92
+ const nextConfig: NextConfig = {
93
+ serverExternalPackages: ['@clickhouse/client'],
94
+ };
95
+ ```
96
+
97
+ ### Do NOT call `initTelemetry` in `instrumentation.ts`
98
+
99
+ On Vercel serverless, `instrumentation.ts` runs in a **separate module scope** from route handlers. Singletons set there are invisible to your handlers.
100
+
101
+ Call `initTelemetry()` in the same module that imports your route wrappers:
102
+
103
+ ```typescript
104
+ // lib/telemetry.ts — CORRECT
105
+ import { initTelemetry, withTelemetry } from '@merit-systems/x402-server-telemetry';
106
+ initTelemetry({ clickhouse: { ... } });
107
+ export { withTelemetry };
108
+ ```
109
+
110
+ ```typescript
111
+ // instrumentation.ts — WRONG: singleton won't be shared with handlers
112
+ import { initTelemetry } from '@merit-systems/x402-server-telemetry';
113
+ export async function register() {
114
+ initTelemetry({ clickhouse: { ... } }); // handlers can't see this
115
+ }
116
+ ```
117
+
118
+ ### Subpath exports isolate heavy deps
119
+
120
+ The `/siwx` and `/builder` entrypoints have additional peer dependencies. If you only use the core `withTelemetry`, you don't need `zod`, `@x402/next`, or `@x402/extensions` installed.
121
+
122
+ ### After updating, commit both `package.json` and lockfile
123
+
124
+ `pnpm update @merit-systems/x402-server-telemetry` bumps the version specifier in **both** `package.json` and `pnpm-lock.yaml`. Vercel's `frozen-lockfile` mode will reject deploys if only the lockfile is committed. Always:
125
+
126
+ ```bash
127
+ git add package.json pnpm-lock.yaml
128
+ ```
@@ -0,0 +1,58 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { ZodType } from 'zod';
3
+ import { a as TelemetryContext } from './types-Bl8IwXin.mjs';
4
+
5
+ /**
6
+ * Convenience route builder that composes telemetry + validation + x402 wrapping.
7
+ * This is optional — servers can use withTelemetry directly.
8
+ *
9
+ * Import from '@agentcash/telemetry/builder'.
10
+ * Requires peer deps: @x402/next, zod (^4), @x402/extensions
11
+ */
12
+
13
+ declare class HttpError extends Error {
14
+ status: number;
15
+ constructor(message: string, status: number);
16
+ }
17
+ type AcceptsOption = {
18
+ amount: string;
19
+ network: string;
20
+ asset?: string;
21
+ };
22
+ type BuilderConfig<TBody = unknown, TQuery = unknown, TOutput = unknown> = {
23
+ accepts: AcceptsOption[];
24
+ bodySchema?: ZodType<TBody>;
25
+ querySchema?: ZodType<TQuery>;
26
+ outputSchema?: ZodType<TOutput>;
27
+ outputExample?: TOutput;
28
+ description?: string;
29
+ };
30
+ type HandlerContext<TBody, TQuery> = {
31
+ body: TBody;
32
+ query: TQuery;
33
+ request: NextRequest;
34
+ telemetry: TelemetryContext;
35
+ };
36
+ type HandlerFn<TBody, TQuery, TResponse> = (ctx: HandlerContext<TBody, TQuery>) => Promise<TResponse>;
37
+ interface RouteBuilderOptions {
38
+ /** The x402 resource server instance (from @x402/core/server). Required when using .price(). */
39
+ x402Server?: unknown;
40
+ }
41
+ declare class RouteBuilder<TBody = unknown, TQuery = unknown, TOutput = unknown> {
42
+ private config;
43
+ private options;
44
+ constructor(config?: Partial<BuilderConfig<TBody, TQuery, TOutput>>, options?: RouteBuilderOptions);
45
+ price(amount: string, network: string, asset?: string): RouteBuilder<TBody, TQuery, TOutput>;
46
+ accepts(options: AcceptsOption[]): RouteBuilder<TBody, TQuery, TOutput>;
47
+ body<T>(schema: ZodType<T>): RouteBuilder<T, TQuery, TOutput>;
48
+ query<T>(schema: ZodType<T>): RouteBuilder<TBody, T, TOutput>;
49
+ output<T>(schema: ZodType<T>, example?: T): RouteBuilder<TBody, TQuery, T>;
50
+ description(text: string): RouteBuilder<TBody, TQuery, TOutput>;
51
+ handler<TResponse>(fn: HandlerFn<TBody, TQuery, TResponse>): (request: NextRequest) => Promise<NextResponse>;
52
+ }
53
+ /**
54
+ * Create a new route builder instance.
55
+ */
56
+ declare function createRouteBuilder(options?: RouteBuilderOptions): RouteBuilder<unknown, unknown, unknown>;
57
+
58
+ export { HttpError, type RouteBuilderOptions, createRouteBuilder };
@@ -0,0 +1,58 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { ZodType } from 'zod';
3
+ import { a as TelemetryContext } from './types-Bl8IwXin.js';
4
+
5
+ /**
6
+ * Convenience route builder that composes telemetry + validation + x402 wrapping.
7
+ * This is optional — servers can use withTelemetry directly.
8
+ *
9
+ * Import from '@agentcash/telemetry/builder'.
10
+ * Requires peer deps: @x402/next, zod (^4), @x402/extensions
11
+ */
12
+
13
+ declare class HttpError extends Error {
14
+ status: number;
15
+ constructor(message: string, status: number);
16
+ }
17
+ type AcceptsOption = {
18
+ amount: string;
19
+ network: string;
20
+ asset?: string;
21
+ };
22
+ type BuilderConfig<TBody = unknown, TQuery = unknown, TOutput = unknown> = {
23
+ accepts: AcceptsOption[];
24
+ bodySchema?: ZodType<TBody>;
25
+ querySchema?: ZodType<TQuery>;
26
+ outputSchema?: ZodType<TOutput>;
27
+ outputExample?: TOutput;
28
+ description?: string;
29
+ };
30
+ type HandlerContext<TBody, TQuery> = {
31
+ body: TBody;
32
+ query: TQuery;
33
+ request: NextRequest;
34
+ telemetry: TelemetryContext;
35
+ };
36
+ type HandlerFn<TBody, TQuery, TResponse> = (ctx: HandlerContext<TBody, TQuery>) => Promise<TResponse>;
37
+ interface RouteBuilderOptions {
38
+ /** The x402 resource server instance (from @x402/core/server). Required when using .price(). */
39
+ x402Server?: unknown;
40
+ }
41
+ declare class RouteBuilder<TBody = unknown, TQuery = unknown, TOutput = unknown> {
42
+ private config;
43
+ private options;
44
+ constructor(config?: Partial<BuilderConfig<TBody, TQuery, TOutput>>, options?: RouteBuilderOptions);
45
+ price(amount: string, network: string, asset?: string): RouteBuilder<TBody, TQuery, TOutput>;
46
+ accepts(options: AcceptsOption[]): RouteBuilder<TBody, TQuery, TOutput>;
47
+ body<T>(schema: ZodType<T>): RouteBuilder<T, TQuery, TOutput>;
48
+ query<T>(schema: ZodType<T>): RouteBuilder<TBody, T, TOutput>;
49
+ output<T>(schema: ZodType<T>, example?: T): RouteBuilder<TBody, TQuery, T>;
50
+ description(text: string): RouteBuilder<TBody, TQuery, TOutput>;
51
+ handler<TResponse>(fn: HandlerFn<TBody, TQuery, TResponse>): (request: NextRequest) => Promise<NextResponse>;
52
+ }
53
+ /**
54
+ * Create a new route builder instance.
55
+ */
56
+ declare function createRouteBuilder(options?: RouteBuilderOptions): RouteBuilder<unknown, unknown, unknown>;
57
+
58
+ export { HttpError, type RouteBuilderOptions, createRouteBuilder };
@@ -0,0 +1,218 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+
4
+
5
+ var _chunkGE7VBMQPjs = require('./chunk-GE7VBMQP.js');
6
+ require('./chunk-P63MLKU3.js');
7
+
8
+ // src/route-builder.ts
9
+ var _server = require('next/server');
10
+ var _next = require('@x402/next');
11
+ var _zod = require('zod');
12
+ var _bazaar = require('@x402/extensions/bazaar');
13
+ var HttpError = class extends Error {
14
+ constructor(message, status) {
15
+ super(message);
16
+ this.status = status;
17
+ this.name = "HttpError";
18
+ }
19
+ };
20
+ function formatValidationError(error) {
21
+ const issues = error.issues;
22
+ if (issues.length === 0) return "Validation failed";
23
+ if (issues.length === 1) {
24
+ const issue = issues[0];
25
+ const path = issue.path.join(".");
26
+ const field = path || "request";
27
+ if (issue.message) return `${field}: ${issue.message}`;
28
+ return `${field}: Invalid value`;
29
+ }
30
+ const errors = issues.map((issue) => {
31
+ const path = issue.path.join(".");
32
+ const field = path || "request";
33
+ return issue.message ? `${field}: ${issue.message}` : `${field}: Invalid`;
34
+ });
35
+ return `Validation failed: ${errors.join("; ")}`;
36
+ }
37
+ var RouteBuilder = class _RouteBuilder {
38
+
39
+
40
+ constructor(config, options) {
41
+ this.config = { accepts: [], ...config };
42
+ this.options = _nullishCoalesce(options, () => ( {}));
43
+ }
44
+ price(amount, network, asset) {
45
+ return new _RouteBuilder(
46
+ { ...this.config, accepts: [{ amount, network, asset }] },
47
+ this.options
48
+ );
49
+ }
50
+ accepts(options) {
51
+ return new _RouteBuilder(
52
+ { ...this.config, accepts: options },
53
+ this.options
54
+ );
55
+ }
56
+ body(schema) {
57
+ return new _RouteBuilder(
58
+ { ...this.config, bodySchema: schema },
59
+ this.options
60
+ );
61
+ }
62
+ query(schema) {
63
+ return new _RouteBuilder(
64
+ { ...this.config, querySchema: schema },
65
+ this.options
66
+ );
67
+ }
68
+ output(schema, example) {
69
+ return new _RouteBuilder(
70
+ {
71
+ ...this.config,
72
+ outputSchema: schema,
73
+ outputExample: example
74
+ },
75
+ this.options
76
+ );
77
+ }
78
+ description(text) {
79
+ return new _RouteBuilder(
80
+ { ...this.config, description: text },
81
+ this.options
82
+ );
83
+ }
84
+ handler(fn) {
85
+ const { accepts, bodySchema, querySchema, outputSchema, outputExample, description } = this.config;
86
+ const coreHandler = async (request) => {
87
+ const meta = _chunkGE7VBMQPjs.extractRequestMeta.call(void 0, request);
88
+ const ctx = _chunkGE7VBMQPjs.buildTelemetryContext.call(void 0, meta);
89
+ const log = (status, responseBody, resp) => {
90
+ _chunkGE7VBMQPjs.recordInvocation.call(void 0, meta, requestBodyString, {
91
+ status,
92
+ body: responseBody,
93
+ headers: JSON.stringify(Object.fromEntries(resp.headers.entries())),
94
+ contentType: _nullishCoalesce(resp.headers.get("content-type"), () => ( null))
95
+ });
96
+ };
97
+ let body = void 0;
98
+ let query = void 0;
99
+ let requestBodyString = null;
100
+ if (bodySchema) {
101
+ let rawBody;
102
+ try {
103
+ rawBody = await request.json();
104
+ requestBodyString = JSON.stringify(rawBody);
105
+ } catch (e) {
106
+ const errorResp = _server.NextResponse.json(
107
+ { success: false, error: "Invalid JSON body" },
108
+ { status: 400 }
109
+ );
110
+ log(400, JSON.stringify({ success: false, error: "Invalid JSON body" }), errorResp);
111
+ return errorResp;
112
+ }
113
+ const parsed = bodySchema.safeParse(rawBody);
114
+ if (!parsed.success) {
115
+ const message = formatValidationError(parsed.error);
116
+ const errorBody = {
117
+ success: false,
118
+ error: "Validation failed",
119
+ message,
120
+ details: parsed.error.flatten()
121
+ };
122
+ const errorResp = _server.NextResponse.json(errorBody, { status: 400 });
123
+ log(400, JSON.stringify(errorBody), errorResp);
124
+ return errorResp;
125
+ }
126
+ body = parsed.data;
127
+ }
128
+ if (querySchema) {
129
+ const searchParams = Object.fromEntries(request.nextUrl.searchParams);
130
+ const parsed = querySchema.safeParse(searchParams);
131
+ if (!parsed.success) {
132
+ const message = formatValidationError(parsed.error);
133
+ const errorBody = {
134
+ success: false,
135
+ error: "Query validation failed",
136
+ message,
137
+ details: parsed.error.flatten()
138
+ };
139
+ const errorResp = _server.NextResponse.json(errorBody, { status: 400 });
140
+ log(400, JSON.stringify(errorBody), errorResp);
141
+ return errorResp;
142
+ }
143
+ query = parsed.data;
144
+ }
145
+ let response;
146
+ try {
147
+ response = await fn({ body, query, request, telemetry: ctx });
148
+ } catch (error) {
149
+ const message = error instanceof Error ? error.message : "Unknown error";
150
+ const status = error instanceof HttpError ? error.status : 500;
151
+ const errorBody = { success: false, error: message };
152
+ const errorResp = _server.NextResponse.json(errorBody, { status });
153
+ log(status, JSON.stringify(errorBody), errorResp);
154
+ return errorResp;
155
+ }
156
+ if (response && typeof response === "object" && "success" in response && !response.success) {
157
+ const errorResp = _server.NextResponse.json(response, { status: 500 });
158
+ log(500, JSON.stringify(response), errorResp);
159
+ return errorResp;
160
+ }
161
+ const successResp = _server.NextResponse.json(response);
162
+ let responseBodyString = null;
163
+ try {
164
+ responseBodyString = JSON.stringify(response);
165
+ } catch (e2) {
166
+ }
167
+ log(200, responseBodyString, successResp);
168
+ return successResp;
169
+ };
170
+ if (accepts.length === 0) {
171
+ return coreHandler;
172
+ }
173
+ const X402_BYPASS = process.env.X402_BYPASS === "true";
174
+ if (X402_BYPASS) {
175
+ return coreHandler;
176
+ }
177
+ const X402_PAYEE_ADDRESS = process.env.X402_PAYEE_ADDRESS;
178
+ if (!X402_PAYEE_ADDRESS) {
179
+ throw new Error("X402_PAYEE_ADDRESS environment variable is required when using .price()");
180
+ }
181
+ const routeConfig = {
182
+ description,
183
+ accepts: accepts.map(({ amount, network, asset }) => ({
184
+ scheme: "exact",
185
+ network,
186
+ price: amount,
187
+ payTo: X402_PAYEE_ADDRESS,
188
+ ...asset && { extra: { asset } }
189
+ })),
190
+ extensions: buildDiscoveryExtensions(bodySchema, querySchema, outputSchema, outputExample)
191
+ };
192
+ if (!this.options.x402Server) {
193
+ throw new Error(
194
+ "x402Server is required when using .price(). Pass it to createRouteBuilder({ x402Server })."
195
+ );
196
+ }
197
+ return _next.withX402.call(void 0, coreHandler, routeConfig, this.options.x402Server);
198
+ }
199
+ };
200
+ function buildDiscoveryExtensions(bodySchema, querySchema, outputSchema, outputExample) {
201
+ const inputJsonSchema = bodySchema ? _zod.z.toJSONSchema(bodySchema, { target: "draft-2020-12" }) : querySchema ? _zod.z.toJSONSchema(querySchema, { target: "draft-2020-12" }) : void 0;
202
+ const outputJsonSchema = outputSchema ? _zod.z.toJSONSchema(outputSchema, { target: "draft-2020-12" }) : void 0;
203
+ if (!inputJsonSchema) return void 0;
204
+ const config = {
205
+ bodyType: bodySchema ? "json" : void 0,
206
+ inputSchema: inputJsonSchema,
207
+ output: outputJsonSchema ? { schema: outputJsonSchema, example: _nullishCoalesce(outputExample, () => ( {})) } : void 0
208
+ };
209
+ return { ..._bazaar.declareDiscoveryExtension.call(void 0, config) };
210
+ }
211
+ function createRouteBuilder(options) {
212
+ return new RouteBuilder(void 0, options);
213
+ }
214
+
215
+
216
+
217
+ exports.HttpError = HttpError; exports.createRouteBuilder = createRouteBuilder;
218
+ //# sourceMappingURL=builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/samragsdale/Documents/Code/merit-systems/agentcash-telemetry/dist/builder.js","../src/route-builder.ts"],"names":[],"mappings":"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","file":"/Users/samragsdale/Documents/Code/merit-systems/agentcash-telemetry/dist/builder.js","sourcesContent":[null,"/**\n * Convenience route builder that composes telemetry + validation + x402 wrapping.\n * This is optional — servers can use withTelemetry directly.\n *\n * Import from '@agentcash/telemetry/builder'.\n * Requires peer deps: @x402/next, zod (^4), @x402/extensions\n */\n\nimport { type NextRequest, NextResponse } from 'next/server';\nimport { withX402 } from '@x402/next';\nimport { z, type ZodType } from 'zod';\nimport { declareDiscoveryExtension } from '@x402/extensions/bazaar';\nimport type { TelemetryContext } from './types';\nimport { extractRequestMeta, buildTelemetryContext, recordInvocation } from './telemetry-core';\n\nexport class HttpError extends Error {\n constructor(\n message: string,\n public status: number,\n ) {\n super(message);\n this.name = 'HttpError';\n }\n}\n\ntype AcceptsOption = {\n amount: string;\n network: string;\n asset?: string;\n};\n\ntype BuilderConfig<TBody = unknown, TQuery = unknown, TOutput = unknown> = {\n accepts: AcceptsOption[];\n bodySchema?: ZodType<TBody>;\n querySchema?: ZodType<TQuery>;\n outputSchema?: ZodType<TOutput>;\n outputExample?: TOutput;\n description?: string;\n};\n\ntype HandlerContext<TBody, TQuery> = {\n body: TBody;\n query: TQuery;\n request: NextRequest;\n telemetry: TelemetryContext;\n};\n\ntype HandlerFn<TBody, TQuery, TResponse> = (\n ctx: HandlerContext<TBody, TQuery>,\n) => Promise<TResponse>;\n\nfunction formatValidationError(error: import('zod').ZodError): string {\n const issues = error.issues;\n if (issues.length === 0) return 'Validation failed';\n\n if (issues.length === 1) {\n const issue = issues[0];\n const path = issue.path.join('.');\n const field = path || 'request';\n if (issue.message) return `${field}: ${issue.message}`;\n return `${field}: Invalid value`;\n }\n\n const errors = issues.map((issue) => {\n const path = issue.path.join('.');\n const field = path || 'request';\n return issue.message ? `${field}: ${issue.message}` : `${field}: Invalid`;\n });\n\n return `Validation failed: ${errors.join('; ')}`;\n}\n\nexport interface RouteBuilderOptions {\n /** The x402 resource server instance (from @x402/core/server). Required when using .price(). */\n x402Server?: unknown;\n}\n\nclass RouteBuilder<TBody = unknown, TQuery = unknown, TOutput = unknown> {\n private config: BuilderConfig<TBody, TQuery, TOutput>;\n private options: RouteBuilderOptions;\n\n constructor(\n config?: Partial<BuilderConfig<TBody, TQuery, TOutput>>,\n options?: RouteBuilderOptions,\n ) {\n this.config = { accepts: [], ...config };\n this.options = options ?? {};\n }\n\n price(amount: string, network: string, asset?: string) {\n return new RouteBuilder<TBody, TQuery, TOutput>(\n { ...this.config, accepts: [{ amount, network, asset }] },\n this.options,\n );\n }\n\n accepts(options: AcceptsOption[]) {\n return new RouteBuilder<TBody, TQuery, TOutput>(\n { ...this.config, accepts: options },\n this.options,\n );\n }\n\n body<T>(schema: ZodType<T>) {\n return new RouteBuilder<T, TQuery, TOutput>(\n { ...this.config, bodySchema: schema as ZodType<unknown> } as BuilderConfig<\n T,\n TQuery,\n TOutput\n >,\n this.options,\n );\n }\n\n query<T>(schema: ZodType<T>) {\n return new RouteBuilder<TBody, T, TOutput>(\n { ...this.config, querySchema: schema as ZodType<unknown> } as BuilderConfig<\n TBody,\n T,\n TOutput\n >,\n this.options,\n );\n }\n\n output<T>(schema: ZodType<T>, example?: T) {\n return new RouteBuilder<TBody, TQuery, T>(\n {\n ...this.config,\n outputSchema: schema as ZodType<unknown>,\n outputExample: example,\n } as BuilderConfig<TBody, TQuery, T>,\n this.options,\n );\n }\n\n description(text: string) {\n return new RouteBuilder<TBody, TQuery, TOutput>(\n { ...this.config, description: text },\n this.options,\n );\n }\n\n handler<TResponse>(fn: HandlerFn<TBody, TQuery, TResponse>) {\n const { accepts, bodySchema, querySchema, outputSchema, outputExample, description } =\n this.config;\n\n const coreHandler = async (request: NextRequest): Promise<NextResponse> => {\n const meta = extractRequestMeta(request);\n const ctx = buildTelemetryContext(meta);\n\n const log = (status: number, responseBody: string | null, resp: NextResponse) => {\n recordInvocation(meta, requestBodyString, {\n status,\n body: responseBody,\n headers: JSON.stringify(Object.fromEntries(resp.headers.entries())),\n contentType: resp.headers.get('content-type') ?? null,\n });\n };\n\n // Parse and validate body\n let body: TBody = undefined as TBody;\n let query: TQuery = undefined as TQuery;\n let requestBodyString: string | null = null;\n\n if (bodySchema) {\n let rawBody: unknown;\n try {\n rawBody = await request.json();\n requestBodyString = JSON.stringify(rawBody);\n } catch {\n const errorResp = NextResponse.json(\n { success: false, error: 'Invalid JSON body' },\n { status: 400 },\n );\n log(400, JSON.stringify({ success: false, error: 'Invalid JSON body' }), errorResp);\n return errorResp;\n }\n\n const parsed = bodySchema.safeParse(rawBody);\n if (!parsed.success) {\n const message = formatValidationError(parsed.error);\n const errorBody = {\n success: false,\n error: 'Validation failed',\n message,\n details: parsed.error.flatten(),\n };\n const errorResp = NextResponse.json(errorBody, { status: 400 });\n log(400, JSON.stringify(errorBody), errorResp);\n return errorResp;\n }\n body = parsed.data;\n }\n\n // Parse and validate query\n if (querySchema) {\n const searchParams = Object.fromEntries(request.nextUrl.searchParams);\n const parsed = querySchema.safeParse(searchParams);\n if (!parsed.success) {\n const message = formatValidationError(parsed.error);\n const errorBody = {\n success: false,\n error: 'Query validation failed',\n message,\n details: parsed.error.flatten(),\n };\n const errorResp = NextResponse.json(errorBody, { status: 400 });\n log(400, JSON.stringify(errorBody), errorResp);\n return errorResp;\n }\n query = parsed.data;\n }\n\n // Execute user handler\n let response: TResponse;\n try {\n response = await fn({ body, query, request, telemetry: ctx });\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n const status = error instanceof HttpError ? error.status : 500;\n const errorBody = { success: false, error: message };\n const errorResp = NextResponse.json(errorBody, { status });\n log(status, JSON.stringify(errorBody), errorResp);\n return errorResp;\n }\n\n // Detect { success: false } to prevent x402 settlement\n if (\n response &&\n typeof response === 'object' &&\n 'success' in response &&\n !(response as { success: boolean }).success\n ) {\n const errorResp = NextResponse.json(response, { status: 500 });\n log(500, JSON.stringify(response), errorResp);\n return errorResp;\n }\n\n // Success\n const successResp = NextResponse.json(response);\n let responseBodyString: string | null = null;\n try {\n responseBodyString = JSON.stringify(response);\n } catch {\n // Not serializable — that's fine\n }\n log(200, responseBodyString, successResp);\n return successResp;\n };\n\n // If no pricing, return the core handler directly (no x402 wrapping)\n if (accepts.length === 0) {\n return coreHandler;\n }\n\n // Wrap with x402\n const X402_BYPASS = process.env.X402_BYPASS === 'true';\n if (X402_BYPASS) {\n return coreHandler;\n }\n\n const X402_PAYEE_ADDRESS = process.env.X402_PAYEE_ADDRESS;\n if (!X402_PAYEE_ADDRESS) {\n throw new Error('X402_PAYEE_ADDRESS environment variable is required when using .price()');\n }\n\n const routeConfig = {\n description,\n accepts: accepts.map(({ amount, network, asset }) => ({\n scheme: 'exact' as const,\n network: network as `${string}:${string}`,\n price: amount,\n payTo: X402_PAYEE_ADDRESS,\n ...(asset && { extra: { asset } }),\n })),\n extensions: buildDiscoveryExtensions(bodySchema, querySchema, outputSchema, outputExample),\n };\n\n if (!this.options.x402Server) {\n throw new Error(\n 'x402Server is required when using .price(). Pass it to createRouteBuilder({ x402Server }).',\n );\n }\n\n return withX402(coreHandler, routeConfig, this.options.x402Server as never);\n }\n}\n\nfunction buildDiscoveryExtensions(\n bodySchema?: ZodType<unknown>,\n querySchema?: ZodType<unknown>,\n outputSchema?: ZodType<unknown>,\n outputExample?: unknown,\n): Record<string, unknown> | undefined {\n const inputJsonSchema = bodySchema\n ? z.toJSONSchema(bodySchema, { target: 'draft-2020-12' })\n : querySchema\n ? z.toJSONSchema(querySchema, { target: 'draft-2020-12' })\n : undefined;\n\n const outputJsonSchema = outputSchema\n ? z.toJSONSchema(outputSchema, { target: 'draft-2020-12' })\n : undefined;\n\n if (!inputJsonSchema) return undefined;\n\n const config = {\n bodyType: bodySchema ? 'json' : undefined,\n inputSchema: inputJsonSchema,\n output: outputJsonSchema\n ? { schema: outputJsonSchema, example: outputExample ?? {} }\n : undefined,\n };\n\n return { ...declareDiscoveryExtension(config as never) };\n}\n\n/**\n * Create a new route builder instance.\n */\nexport function createRouteBuilder(options?: RouteBuilderOptions) {\n return new RouteBuilder(undefined, options);\n}\n"]}