@agentcash/telemetry 0.0.0-pr-16-20260520161400

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,159 @@
1
+ # @agentcash/telemetry
2
+
3
+ [![npm](https://img.shields.io/npm/v/@agentcash/telemetry)](https://www.npmjs.com/package/@agentcash/telemetry)
4
+
5
+ ClickHouse telemetry for x402/MPP/SIWX API services. Logs invocations to ClickHouse, extracts verified wallets from x402 payments and SIWX auth.
6
+
7
+ [Telemetry spec](docs/telemetry-spec.md) | [npm](https://www.npmjs.com/package/@agentcash/telemetry) | [GitHub](https://github.com/Merit-Systems/agentcash-telemetry)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @agentcash/telemetry @clickhouse/client
13
+ ```
14
+
15
+ ## Quick start — Router Plugin (recommended)
16
+
17
+ ```typescript
18
+ import { createRouter } from '@agentcash/router';
19
+ import { createTelemetryPlugin } from '@agentcash/telemetry/plugin';
20
+
21
+ const router = createRouter({
22
+ payeeAddress: '0x...',
23
+ plugin: createTelemetryPlugin({
24
+ clickhouse: {
25
+ url: process.env.TELEM_CLICKHOUSE_URL!,
26
+ database: process.env.TELEM_CLICKHOUSE_DATABASE,
27
+ username: process.env.TELEM_CLICKHOUSE_USERNAME,
28
+ password: process.env.TELEM_CLICKHOUSE_PASSWORD,
29
+ },
30
+ }),
31
+ });
32
+ ```
33
+
34
+ ## Quick start — Legacy wrapper
35
+
36
+ ```typescript
37
+ // lib/telemetry.ts (or wherever your route wrappers live)
38
+ import { initTelemetry, withTelemetry } from '@agentcash/telemetry';
39
+
40
+ initTelemetry({
41
+ clickhouse: {
42
+ url: process.env.TELEM_CLICKHOUSE_URL!,
43
+ database: process.env.TELEM_CLICKHOUSE_DATABASE,
44
+ username: process.env.TELEM_CLICKHOUSE_USERNAME,
45
+ password: process.env.TELEM_CLICKHOUSE_PASSWORD,
46
+ },
47
+ verify: true, // optional — pings ClickHouse on startup, logs success/failure
48
+ });
49
+
50
+ export { withTelemetry };
51
+ ```
52
+
53
+ ```typescript
54
+ // app/api/example/route.ts
55
+ import { withTelemetry } from '@/lib/telemetry';
56
+
57
+ export const POST = withTelemetry(async (request, ctx) => {
58
+ return NextResponse.json(await doWork(request));
59
+ });
60
+ ```
61
+
62
+ ## Four entrypoints
63
+
64
+ ### Router Plugin (`@agentcash/telemetry/plugin`)
65
+
66
+ **Primary integration path.** Hooks into `@agentcash/router`'s orchestrate lifecycle.
67
+
68
+ Requires: `@clickhouse/client`
69
+
70
+ ```typescript
71
+ import { createTelemetryPlugin } from '@agentcash/telemetry/plugin';
72
+ ```
73
+
74
+ - `createTelemetryPlugin(config)` — returns a `RouterPlugin` that captures request metadata, payment verification, settlement, response, errors, alerts, and provider quota
75
+
76
+ ### Core (`@agentcash/telemetry`)
77
+
78
+ Requires: `@clickhouse/client`, `next`
79
+
80
+ ```typescript
81
+ import { initTelemetry, withTelemetry } from '@agentcash/telemetry';
82
+ ```
83
+
84
+ - `initTelemetry(config)` — synchronous, call once at module level. Pass `verify: true` to ping ClickHouse on startup (fire-and-forget, never blocks)
85
+ - `withTelemetry(handler)` — wrap any Next.js route handler
86
+ - `extractVerifiedWallet(headers)` — extract wallet from x402 payment headers
87
+
88
+ ### SIWX (`@agentcash/telemetry/siwx`)
89
+
90
+ Requires: `@x402/extensions`, `@x402/core`
91
+
92
+ ```typescript
93
+ import { withSiwxTelemetry } from '@agentcash/telemetry/siwx';
94
+
95
+ export const GET = withSiwxTelemetry(async (request, ctx) => {
96
+ // ctx.verifiedWallet is guaranteed to be set
97
+ return NextResponse.json(await getJobs(ctx.verifiedWallet));
98
+ });
99
+ ```
100
+
101
+ ### Route Builder (`@agentcash/telemetry/builder`)
102
+
103
+ Requires: `@x402/next`, `zod` (^4), `@x402/extensions`
104
+
105
+ ```typescript
106
+ import { createRouteBuilder } from '@agentcash/telemetry/builder';
107
+
108
+ const route = createRouteBuilder({ x402Server });
109
+
110
+ export const POST = route
111
+ .price('0.05', 'base:8453')
112
+ .body(searchSchema)
113
+ .handler(async ({ body }) => searchPeople(body.query));
114
+ ```
115
+
116
+ ## Next.js integration footguns
117
+
118
+ ### `@clickhouse/client` must be externalized
119
+
120
+ The ClickHouse client uses Node.js native APIs that break when bundled by Next.js. Add to your `next.config`:
121
+
122
+ ```typescript
123
+ const nextConfig: NextConfig = {
124
+ serverExternalPackages: ['@clickhouse/client'],
125
+ };
126
+ ```
127
+
128
+ ### Do NOT call `initTelemetry` in `instrumentation.ts`
129
+
130
+ On Vercel serverless, `instrumentation.ts` runs in a **separate module scope** from route handlers. Singletons set there are invisible to your handlers.
131
+
132
+ Call `initTelemetry()` in the same module that imports your route wrappers:
133
+
134
+ ```typescript
135
+ // lib/telemetry.ts — CORRECT
136
+ import { initTelemetry, withTelemetry } from '@agentcash/telemetry';
137
+ initTelemetry({ clickhouse: { ... } });
138
+ export { withTelemetry };
139
+ ```
140
+
141
+ ```typescript
142
+ // instrumentation.ts — WRONG: singleton won't be shared with handlers
143
+ import { initTelemetry } from '@agentcash/telemetry';
144
+ export async function register() {
145
+ initTelemetry({ clickhouse: { ... } }); // handlers can't see this
146
+ }
147
+ ```
148
+
149
+ ### Subpath exports isolate heavy deps
150
+
151
+ The `/siwx` and `/builder` entrypoints have additional peer dependencies. If you only use the core `withTelemetry` or `./plugin`, you don't need `zod`, `@x402/next`, or `@x402/extensions` installed.
152
+
153
+ ### After updating, commit both `package.json` and lockfile
154
+
155
+ `pnpm update @agentcash/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:
156
+
157
+ ```bash
158
+ git add package.json pnpm-lock.yaml
159
+ ```
@@ -0,0 +1,58 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { ZodType } from 'zod';
3
+ import { a as TelemetryContext } from './types-DnZ7Qg3n.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-DnZ7Qg3n.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 _chunkMBH3LCBQjs = require('./chunk-MBH3LCBQ.js');
6
+ require('./chunk-QEJ7ZGGH.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 = _chunkMBH3LCBQjs.extractRequestMeta.call(void 0, request);
88
+ const ctx = _chunkMBH3LCBQjs.buildTelemetryContext.call(void 0, meta);
89
+ const log = (status, responseBody, resp) => {
90
+ _chunkMBH3LCBQjs.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":["/home/runner/work/agentcash-telemetry/agentcash-telemetry/dist/builder.js","../src/route-builder.ts"],"names":[],"mappings":"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","file":"/home/runner/work/agentcash-telemetry/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"]}