@t2000/serve 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 t2000
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @t2000/serve
2
+
3
+ Merchant-side x402 router for Sui — wrap any API so agents can discover it and pay per call in USDC.
4
+
5
+ ```ts
6
+ // app/api/search/route.ts (Next.js) — a complete paid endpoint
7
+ import { createServeFromEnv } from '@t2000/serve';
8
+
9
+ const serve = createServeFromEnv(); // reads T2000_PAY_TO from env
10
+
11
+ export const POST = serve
12
+ .route({ path: 'search' })
13
+ .paid('0.01') // USDC per call
14
+ .body(searchSchema) // zod v4 / valibot / arktype / anything Standard-Schema
15
+ .handler(async ({ body }) => search(body));
16
+ ```
17
+
18
+ That route now answers x402 payment challenges, validates inputs, verifies and
19
+ settles payments on Sui, and is listable on [mpp.t2000.ai](https://mpp.t2000.ai)
20
+ and [agents.t2000.ai](https://agents.t2000.ai) — where every agent running
21
+ `t2 pay` or the t2000 MCP can find and pay it.
22
+
23
+ ## Why Sui / why this package
24
+
25
+ - **No seller key, no seller gas.** Payment settles sign-then-settle: the buyer
26
+ signs a gasless USDC transfer, this package verifies and submits it. Your
27
+ server never holds a private key and never pays gas. (Every EVM x402 router
28
+ needs an operator wallet key in the server env. This one doesn't.)
29
+ - **No charge on failure.** The handler runs *before* settlement. Invalid body →
30
+ 422, handler throws → 500 — in both cases the buyer's payment is never
31
+ submitted. Getting this wrong by hand is the most common seller bug.
32
+ - **Correct by construction.** Challenge-once + digest-once replay protection,
33
+ structural payment verification (right amount, right recipient, gasless-only,
34
+ framework-calls-only), CORS for browser wallets — all defaults.
35
+
36
+ ## Setup
37
+
38
+ ```bash
39
+ npm install @t2000/serve
40
+ ```
41
+
42
+ | Env var | Required | What it is |
43
+ |---|---|---|
44
+ | `T2000_PAY_TO` | yes | Your Sui address — payments settle here (`t2 address` prints it) |
45
+ | `T2000_NETWORK` | no | `mainnet` (default) or `testnet` |
46
+ | `T2000_BASE_URL` | no | Public URL of the deployed app (used in challenges + discovery) |
47
+ | `KV_REST_API_URL` / `KV_REST_API_TOKEN` | serverless: yes | Upstash-compatible KV for durable replay protection. Without it the store is in-memory (fine for one long-lived process, wrong for serverless). |
48
+
49
+ No wallet yet? `npm i -g @t2000/cli && t2 init` — wallet + free on-chain Agent ID.
50
+
51
+ ## Routes
52
+
53
+ ```ts
54
+ serve.route({ path: 'search' }).paid('0.01').body(schema).handler(fn); // paid
55
+ serve.route({ path: 'health' }).unprotected().handler(() => ({ ok: true })); // free
56
+ ```
57
+
58
+ - Prices are human-unit USDC strings (`'0.01'`), max 6 decimals.
59
+ - Prices above **5 USDC** work but won't list on the mpp.t2000.ai catalog —
60
+ deliverable-priced work belongs in escrow (`t2 service create`).
61
+ - Handlers receive `{ body, req, payer }` — `payer` is the buyer's verified Sui
62
+ address (wallet-based identity, no accounts).
63
+ - Return any JSON-serializable value (wrapped in a 200) or a `Response`.
64
+
65
+ ## Discovery — agents find you
66
+
67
+ ```ts
68
+ // Next.js
69
+ export const GET = serve.openapi(); // app/openapi.json/route.ts
70
+ export const GET = serve.llms(); // app/llms.txt/route.ts
71
+
72
+ // Fetch runtimes (Bun / Deno / Hono / Workers) — one handler for everything:
73
+ Bun.serve({ fetch: serve.fetch });
74
+ app.all('*', (c) => serve.fetch(c.req.raw)); // Hono
75
+ ```
76
+
77
+ `/openapi.json` is OpenAPI 3.1 with the `x-payment-info` pricing extension on
78
+ every paid operation (the shape mpp.t2000.ai indexes); `/llms.txt` is plain-text
79
+ guidance for agents. Pass a JSON Schema as `.body(schema, jsonSchema)` (zod v4:
80
+ `z.toJSONSchema(schema)`) and buyers' agents build request bodies without
81
+ guessing — a wrong guess against a direct seller is a paid error.
82
+
83
+ ## Get listed
84
+
85
+ Deploy, then:
86
+
87
+ ```ts
88
+ console.log(serve.catalogSubmitCommand('https://api.example.com'));
89
+ ```
90
+
91
+ prints the dry-run (`/api/catalog/preview`) and submit (`/api/catalog/submit`)
92
+ curls. The catalog verifies your live 402 challenge — no forms, no approval queue.
93
+
94
+ ## Docs
95
+
96
+ Full guide: [developers.t2000.ai](https://developers.t2000.ai) → Sell to agents.
package/dist/index.cjs ADDED
@@ -0,0 +1,581 @@
1
+ 'use strict';
2
+
3
+ var utils = require('@mysten/sui/utils');
4
+ var server = require('@suimpp/mpp/server');
5
+ var x402 = require('@suimpp/mpp/x402');
6
+ var grpc = require('@mysten/sui/grpc');
7
+
8
+ // src/serve.ts
9
+ function buildOpenApiDocument(serve, origin) {
10
+ const base = serve.baseUrl ?? origin;
11
+ const paths = {};
12
+ for (const route of serve.routes.values()) {
13
+ const { path, priceUsdc, description, inputSchema } = route.meta;
14
+ const operation = {
15
+ operationId: path.replace(/[^a-zA-Z0-9]+/g, "_"),
16
+ summary: description ?? path,
17
+ responses: {
18
+ "200": { description: "Success" },
19
+ ...priceUsdc ? {
20
+ "402": {
21
+ description: 'Payment required \u2014 the body carries an x402 accepts[] envelope (scheme "exact", network sui). Sign it and retry with the X-PAYMENT header.'
22
+ }
23
+ } : {},
24
+ "422": { description: "Invalid request body \u2014 never charged" }
25
+ }
26
+ };
27
+ if (priceUsdc) {
28
+ operation["x-payment-info"] = {
29
+ pricingMode: "fixed",
30
+ price: priceUsdc,
31
+ currency: "USDC",
32
+ protocols: ["mpp", "x402"],
33
+ x402: {
34
+ scheme: "exact",
35
+ network: `sui:${serve.network}`,
36
+ asset: (serve.network === "testnet" ? server.USDC_TESTNET : server.USDC).type,
37
+ payTo: serve.payTo
38
+ }
39
+ };
40
+ }
41
+ if (inputSchema) {
42
+ operation.requestBody = {
43
+ required: true,
44
+ content: { "application/json": { schema: inputSchema } }
45
+ };
46
+ }
47
+ paths[`/${path}`] = { post: operation };
48
+ }
49
+ return {
50
+ openapi: "3.1.0",
51
+ info: {
52
+ title: serve.name ?? "Paid API",
53
+ version: "1.0.0",
54
+ description: serve.description ?? "Agent-payable API. Every paid endpoint answers 402 with an x402 accepts[] envelope; payment settles in USDC on Sui."
55
+ },
56
+ ...base ? { servers: [{ url: base }] } : {},
57
+ paths
58
+ };
59
+ }
60
+ function buildLlmsTxt(serve, origin) {
61
+ const base = serve.baseUrl ?? origin ?? "";
62
+ const lines = [];
63
+ lines.push(`# ${serve.name ?? "Paid API"}`);
64
+ lines.push("");
65
+ if (serve.description) {
66
+ lines.push(serve.description);
67
+ lines.push("");
68
+ }
69
+ lines.push("## How to pay");
70
+ lines.push("");
71
+ lines.push(
72
+ "Endpoints below are paid per call in USDC on Sui (x402). An unpaid request returns",
73
+ "HTTP 402 with an `accepts[]` envelope; sign it and retry with the `X-PAYMENT` header.",
74
+ "Invalid input returns 422 before any payment is taken. A failed call is never charged.",
75
+ "",
76
+ "Easiest client: `npm i -g @t2000/cli` then `t2 pay <url> --method POST --body '{...}'`,",
77
+ "or the t2000 MCP server (`t2000_pay`), or `@t2000/sdk` `pay()`.",
78
+ ""
79
+ );
80
+ lines.push("## Endpoints");
81
+ lines.push("");
82
+ for (const route of serve.routes.values()) {
83
+ const { path, priceUsdc, description, inputSchema } = route.meta;
84
+ const price = priceUsdc ? `${priceUsdc} USDC per call` : "free";
85
+ lines.push(`### POST ${base}/${path} \u2014 ${price}`);
86
+ if (description) lines.push(description);
87
+ if (inputSchema) {
88
+ lines.push("Request body (JSON Schema):");
89
+ lines.push("```json");
90
+ lines.push(JSON.stringify(inputSchema, null, 2));
91
+ lines.push("```");
92
+ }
93
+ lines.push("");
94
+ }
95
+ lines.push(`Discovery: ${base}/openapi.json`);
96
+ return lines.join("\n");
97
+ }
98
+ var FULLNODE_URLS = {
99
+ mainnet: "https://fullnode.mainnet.sui.io:443",
100
+ testnet: "https://fullnode.testnet.sui.io:443"
101
+ };
102
+ var EPOCH_TTL_MS = 10 * 60 * 1e3;
103
+ var state = {};
104
+ function getGrpcClient(network, rpcUrl) {
105
+ const s = state[network] ??= {};
106
+ if (!s.client) {
107
+ s.client = new grpc.SuiGrpcClient({
108
+ baseUrl: rpcUrl ?? FULLNODE_URLS[network],
109
+ network
110
+ });
111
+ }
112
+ return s.client;
113
+ }
114
+ async function getChainInfo(network, rpcUrl) {
115
+ const client = getGrpcClient(network, rpcUrl);
116
+ const s = state[network] ??= {};
117
+ if (!s.chainId) {
118
+ const res = await client.core.getChainIdentifier();
119
+ s.chainId = res.chainIdentifier;
120
+ }
121
+ if (!s.epoch || Date.now() - s.epoch.fetchedAt > EPOCH_TTL_MS) {
122
+ const res = await client.core.getCurrentSystemState();
123
+ s.epoch = { value: String(res.systemState.epoch), fetchedAt: Date.now() };
124
+ }
125
+ return { chain: s.chainId, epoch: s.epoch.value };
126
+ }
127
+ function __resetChainCaches() {
128
+ delete state.mainnet;
129
+ delete state.testnet;
130
+ }
131
+ function __seedChainInfo(network, chain, epoch) {
132
+ const s = state[network] ??= {};
133
+ s.chainId = chain;
134
+ s.epoch = { value: epoch, fetchedAt: Date.now() };
135
+ }
136
+
137
+ // src/route.ts
138
+ var MPP_REPORT_URL = "https://mpp.t2000.ai/api/mpp/report";
139
+ var REPORT_TIMEOUT_MS = 2e3;
140
+ var CATALOG_PRICE_CAP_USDC = 5;
141
+ var CORS_HEADERS = {
142
+ "access-control-allow-origin": "*",
143
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
144
+ "access-control-allow-headers": `content-type, ${x402.X402_PAYMENT_HEADER}`,
145
+ "access-control-expose-headers": `${x402.X402_PAYMENT_RESPONSE_HEADER}, WWW-Authenticate`
146
+ };
147
+ function withCors(response) {
148
+ const headers = new Headers(response.headers);
149
+ for (const [k, v] of Object.entries(CORS_HEADERS)) headers.set(k, v);
150
+ return new Response(response.body, { status: response.status, headers });
151
+ }
152
+ function json(status, body, headers) {
153
+ return withCors(
154
+ new Response(JSON.stringify(body), {
155
+ status,
156
+ headers: { "content-type": "application/json", ...headers }
157
+ })
158
+ );
159
+ }
160
+ function isStandardSchema(schema) {
161
+ return typeof schema === "object" && schema !== null && "~standard" in schema;
162
+ }
163
+ async function validateBody(schema, value) {
164
+ if (isStandardSchema(schema)) {
165
+ const result2 = await schema["~standard"].validate(value);
166
+ if (result2.issues) {
167
+ return { ok: false, message: result2.issues.map((i) => i.message).join("; ") };
168
+ }
169
+ return { ok: true, data: result2.value };
170
+ }
171
+ const result = schema.safeParse(value);
172
+ if (!result.success) {
173
+ return { ok: false, message: result.error.message ?? "invalid body" };
174
+ }
175
+ return { ok: true, data: result.data };
176
+ }
177
+ function assertValidPrice(price, path) {
178
+ if (!/^\d+(\.\d{1,6})?$/.test(price) || Number(price) <= 0) {
179
+ throw new Error(
180
+ `[serve] Route "${path}": price must be a positive decimal USDC string with up to 6 decimals, got "${price}"`
181
+ );
182
+ }
183
+ if (Number(price) > CATALOG_PRICE_CAP_USDC) {
184
+ console.warn(
185
+ `[serve] Route "${path}": price ${price} USDC is above the ${CATALOG_PRICE_CAP_USDC} USDC listing cap \u2014 the route works, but it will not list on mpp.t2000.ai. Job-class work belongs in \`t2 service create\` (escrow).`
186
+ );
187
+ }
188
+ }
189
+ var RouteBuilder = class {
190
+ constructor(runtime, options, register) {
191
+ this.runtime = runtime;
192
+ this.options = options;
193
+ this.register = register;
194
+ }
195
+ priceUsdc;
196
+ bodySchema;
197
+ inputSchema;
198
+ isFree = false;
199
+ /** Charge this many USDC per call (human units, e.g. "0.01"). */
200
+ paid(priceUsdc) {
201
+ assertValidPrice(priceUsdc, this.options.path);
202
+ this.priceUsdc = priceUsdc;
203
+ this.isFree = false;
204
+ return this;
205
+ }
206
+ /** Serve without payment (health checks, previews, docs). */
207
+ unprotected() {
208
+ this.isFree = true;
209
+ this.priceUsdc = void 0;
210
+ return this;
211
+ }
212
+ /**
213
+ * Validate the JSON request body. zod v4 / valibot / arktype / anything
214
+ * Standard-Schema, or anything with a zod-style safeParse.
215
+ *
216
+ * Pass the JSON Schema as the second argument to publish it in
217
+ * /openapi.json + /llms.txt (zod v4: `z.toJSONSchema(schema)`) — buyers'
218
+ * agents build request bodies from it, and the catalog grades listings
219
+ * without one.
220
+ */
221
+ body(schema, jsonSchema) {
222
+ this.bodySchema = schema;
223
+ this.inputSchema = jsonSchema;
224
+ return this;
225
+ }
226
+ handler(fn) {
227
+ if (!this.isFree && !this.priceUsdc) {
228
+ throw new Error(
229
+ `[serve] Route "${this.options.path}": call .paid('<usdc>') or .unprotected() before .handler()`
230
+ );
231
+ }
232
+ const runtime = this.runtime;
233
+ const { path, description } = this.options;
234
+ const priceUsdc = this.priceUsdc;
235
+ const bodySchema = this.bodySchema;
236
+ const route = (async (req) => {
237
+ if (req.method === "OPTIONS") {
238
+ return withCors(new Response(null, { status: 204 }));
239
+ }
240
+ let parsedBody;
241
+ let bodyParseError;
242
+ if (req.method !== "GET" && req.method !== "HEAD") {
243
+ const text = await req.text();
244
+ if (text.trim().length > 0) {
245
+ try {
246
+ parsedBody = JSON.parse(text);
247
+ } catch {
248
+ bodyParseError = "request body is not valid JSON";
249
+ }
250
+ }
251
+ }
252
+ const resource = (runtime.baseUrl ?? new URL(req.url).origin).replace(/\/$/, "") + `/${path}`;
253
+ const runHandler = async (payer) => {
254
+ let validated = parsedBody;
255
+ if (bodySchema) {
256
+ if (bodyParseError) return json(422, { error: bodyParseError });
257
+ const result = await validateBody(bodySchema, parsedBody ?? {});
258
+ if (!result.ok) return json(422, { error: result.message });
259
+ validated = result.data;
260
+ }
261
+ const out = await fn({ body: validated, req, payer });
262
+ if (out instanceof Response) return withCors(out);
263
+ return json(200, out);
264
+ };
265
+ if (!priceUsdc) {
266
+ try {
267
+ return await runHandler();
268
+ } catch (err) {
269
+ console.error(`[serve] Route "${path}" handler failed:`, err);
270
+ return json(500, { error: "internal error" });
271
+ }
272
+ }
273
+ const paymentHeader = req.headers.get(x402.X402_PAYMENT_HEADER);
274
+ if (!paymentHeader) {
275
+ return await respond402(runtime, { resource, priceUsdc });
276
+ }
277
+ if (x402.isX402EscrowHeader(paymentHeader)) {
278
+ return await respond402(runtime, {
279
+ resource,
280
+ priceUsdc,
281
+ error: "this endpoint sells instant per-call work (settle-then-serve), not escrow jobs \u2014 retry with a signed x402 payment"
282
+ });
283
+ }
284
+ let payment;
285
+ try {
286
+ payment = x402.parseX402Header(paymentHeader);
287
+ x402.verifyX402Payment({
288
+ payment,
289
+ expected: {
290
+ challengeId: payment.payload.challengeId,
291
+ amount: priceUsdc,
292
+ currency: runtime.currency,
293
+ recipient: runtime.payTo,
294
+ network: runtime.network
295
+ }
296
+ });
297
+ } catch (err) {
298
+ return await respond402(runtime, {
299
+ resource,
300
+ priceUsdc,
301
+ error: `invalid payment: ${err instanceof Error ? err.message : String(err)}`
302
+ });
303
+ }
304
+ const challengeKey = `challenge:${payment.payload.challengeId}`;
305
+ if (await runtime.store.has(challengeKey)) {
306
+ return await respond402(runtime, {
307
+ resource,
308
+ priceUsdc,
309
+ error: "payment challenge already used \u2014 sign a fresh payment"
310
+ });
311
+ }
312
+ let served;
313
+ try {
314
+ served = await runHandler(payment.payload.senderAddress);
315
+ } catch (err) {
316
+ console.error(`[serve] Route "${path}" handler failed (payment NOT settled):`, err);
317
+ return json(500, { error: "internal error \u2014 you were not charged" });
318
+ }
319
+ if (served.status >= 400) return served;
320
+ let settle;
321
+ try {
322
+ settle = await x402.settleX402Payment({
323
+ payment,
324
+ client: getGrpcClient(runtime.network, runtime.rpcUrl),
325
+ store: runtime.store,
326
+ expected: {
327
+ challengeId: payment.payload.challengeId,
328
+ amount: priceUsdc,
329
+ currency: runtime.currency,
330
+ recipient: runtime.payTo,
331
+ network: runtime.network
332
+ }
333
+ });
334
+ } catch (err) {
335
+ return await respond402(runtime, {
336
+ resource,
337
+ priceUsdc,
338
+ error: `settlement failed: ${err instanceof Error ? err.message : String(err)}`
339
+ });
340
+ }
341
+ try {
342
+ await runtime.store.set(challengeKey);
343
+ } catch {
344
+ }
345
+ if (runtime.report) {
346
+ reportPayment(settle.transaction, resource);
347
+ }
348
+ const headers = new Headers(served.headers);
349
+ headers.set(x402.X402_PAYMENT_RESPONSE_HEADER, x402.encodeX402Response(settle));
350
+ return new Response(served.body, { status: served.status, headers });
351
+ });
352
+ route.meta = { path, priceUsdc, description, bodySchema, inputSchema: this.inputSchema };
353
+ this.register(route);
354
+ return route;
355
+ }
356
+ };
357
+ async function respond402(runtime, args) {
358
+ try {
359
+ const { chain, epoch } = await getChainInfo(runtime.network, runtime.rpcUrl);
360
+ const requirements = x402.createX402Requirements({
361
+ challengeId: crypto.randomUUID(),
362
+ amount: args.priceUsdc,
363
+ currency: runtime.currency,
364
+ recipient: runtime.payTo,
365
+ resource: args.resource,
366
+ network: runtime.network,
367
+ chain,
368
+ currentEpoch: epoch
369
+ });
370
+ return json(402, {
371
+ x402Version: x402.X402_VERSION,
372
+ error: args.error ?? "Payment required",
373
+ accepts: [requirements]
374
+ });
375
+ } catch (err) {
376
+ console.error("[serve] failed to build 402 challenge:", err);
377
+ return json(503, { error: "payment challenge temporarily unavailable" });
378
+ }
379
+ }
380
+ function reportPayment(digest, url) {
381
+ void fetch(MPP_REPORT_URL, {
382
+ method: "POST",
383
+ headers: { "content-type": "application/json" },
384
+ body: JSON.stringify({ digest, url }),
385
+ signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
386
+ }).catch(() => {
387
+ });
388
+ }
389
+ var DEFAULT_TTL_SECONDS = 72 * 60 * 60;
390
+ var PREFIX = "serve:digest:";
391
+ var UpstashDigestStore = class {
392
+ url;
393
+ token;
394
+ ttlSeconds;
395
+ constructor(options) {
396
+ this.url = options.url.replace(/\/$/, "");
397
+ this.token = options.token;
398
+ this.ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
399
+ }
400
+ async command(segments) {
401
+ const res = await fetch(`${this.url}/${segments.map(encodeURIComponent).join("/")}`, {
402
+ headers: { authorization: `Bearer ${this.token}` }
403
+ });
404
+ if (!res.ok) {
405
+ throw new Error(`Upstash command failed: ${res.status} ${await res.text()}`);
406
+ }
407
+ const body = await res.json();
408
+ if (body.error) throw new Error(`Upstash error: ${body.error}`);
409
+ return body.result;
410
+ }
411
+ async has(digest) {
412
+ const result = await this.command(["GET", PREFIX + digest]);
413
+ return result !== null && result !== void 0;
414
+ }
415
+ async set(digest) {
416
+ const result = await this.command([
417
+ "SET",
418
+ PREFIX + digest,
419
+ "1",
420
+ "EX",
421
+ String(this.ttlSeconds),
422
+ "NX"
423
+ ]);
424
+ if (result === null || result === void 0) {
425
+ throw new Error(`Digest already used: ${digest}`);
426
+ }
427
+ }
428
+ };
429
+
430
+ // src/serve.ts
431
+ var Serve = class {
432
+ runtime;
433
+ /** Every route built through this instance, keyed by path (discovery). */
434
+ routes = /* @__PURE__ */ new Map();
435
+ payTo;
436
+ network;
437
+ name;
438
+ description;
439
+ baseUrl;
440
+ constructor(config) {
441
+ if (!config.payTo || !utils.isValidSuiAddress(config.payTo)) {
442
+ throw new Error(
443
+ `[serve] payTo must be a valid Sui address (the wallet your payments settle to), got "${config.payTo}"`
444
+ );
445
+ }
446
+ this.payTo = utils.normalizeSuiAddress(config.payTo);
447
+ this.network = config.network ?? "mainnet";
448
+ this.name = config.name;
449
+ this.description = config.description;
450
+ this.baseUrl = config.baseUrl?.replace(/\/$/, "");
451
+ const currency = this.network === "testnet" ? server.USDC_TESTNET : server.USDC;
452
+ this.runtime = {
453
+ payTo: this.payTo,
454
+ network: this.network,
455
+ currency,
456
+ store: config.store ?? new server.InMemoryDigestStore(),
457
+ baseUrl: this.baseUrl,
458
+ rpcUrl: config.rpcUrl,
459
+ report: config.report ?? true
460
+ };
461
+ }
462
+ /** Start building a route. Chain `.paid()` / `.body()` / `.handler()`. */
463
+ route(options) {
464
+ const path = options.path.replace(/^\/+/, "").replace(/\/+$/, "");
465
+ if (!path) throw new Error("[serve] route path must be non-empty");
466
+ return new RouteBuilder({ ...this.runtime }, { ...options, path }, (route) => {
467
+ this.routes.set(path, route);
468
+ });
469
+ }
470
+ /**
471
+ * Discovery: GET handler for /openapi.json. OpenAPI 3.1 with the
472
+ * `x-payment-info` pricing extension on every paid operation — the shape
473
+ * the mpp.t2000.ai catalog (and x402 tooling generally) indexes.
474
+ *
475
+ * export const GET = serve.openapi(); // app/openapi.json/route.ts
476
+ */
477
+ openapi() {
478
+ return (req) => new Response(JSON.stringify(buildOpenApiDocument(this, new URL(req.url).origin), null, 2), {
479
+ headers: { "content-type": "application/json", "access-control-allow-origin": "*" }
480
+ });
481
+ }
482
+ /**
483
+ * Discovery: GET handler for /llms.txt — plain-text guidance agents read
484
+ * to understand what the API sells, what it costs, and how to pay.
485
+ *
486
+ * export const GET = serve.llms(); // app/llms.txt/route.ts
487
+ */
488
+ llms() {
489
+ return (req) => new Response(buildLlmsTxt(this, new URL(req.url).origin), {
490
+ headers: { "content-type": "text/plain; charset=utf-8", "access-control-allow-origin": "*" }
491
+ });
492
+ }
493
+ /**
494
+ * One fetch handler for the whole app — routes + discovery docs. For
495
+ * fetch-native runtimes (Bun.serve, Deno.serve, Hono, Cloudflare Workers):
496
+ *
497
+ * Bun.serve({ fetch: serve.fetch });
498
+ * app.all('*', (c) => serve.fetch(c.req.raw)); // Hono
499
+ *
500
+ * Next.js apps can skip this and export route handlers directly.
501
+ */
502
+ fetch = async (req) => {
503
+ const pathname = new URL(req.url).pathname.replace(/^\/+/, "").replace(/\/+$/, "");
504
+ if (pathname === "openapi.json") return this.openapi()(req);
505
+ if (pathname === "llms.txt") return this.llms()(req);
506
+ const route = this.routes.get(pathname);
507
+ if (route) return route(req);
508
+ return new Response(
509
+ JSON.stringify({ error: "not found", discovery: ["/openapi.json", "/llms.txt"] }),
510
+ { status: 404, headers: { "content-type": "application/json" } }
511
+ );
512
+ };
513
+ /**
514
+ * The curl that lists this API on mpp.t2000.ai / agents.t2000.ai once it
515
+ * is deployed. Listing is a separate, explicit step — the catalog gates
516
+ * (url · probe · dialect · price-cap), not this package, decide outcomes.
517
+ * Dry-run first with /api/catalog/preview.
518
+ */
519
+ catalogSubmitCommand(deployedUrl) {
520
+ const base = (deployedUrl ?? this.baseUrl ?? "https://<your-deployed-app>").replace(/\/$/, "");
521
+ const paidPaths = [...this.routes.values()].filter((r) => r.meta.priceUsdc);
522
+ const example = paidPaths[0]?.meta.path ?? "<route>";
523
+ return [
524
+ "# Dry-run (shows gate results, changes nothing):",
525
+ `curl -X POST https://mpp.t2000.ai/api/catalog/preview -H 'content-type: application/json' -d '{"url":"${base}/${example}"}'`,
526
+ "# List for real:",
527
+ `curl -X POST https://mpp.t2000.ai/api/catalog/submit -H 'content-type: application/json' -d '{"url":"${base}/${example}"}'`
528
+ ].join("\n");
529
+ }
530
+ };
531
+ function createServe(config) {
532
+ return new Serve(config);
533
+ }
534
+ function createServeFromEnv(env = process.env) {
535
+ const read = (key) => {
536
+ const v = env[key]?.trim();
537
+ return v && v.length > 0 ? v : void 0;
538
+ };
539
+ const payTo = read("T2000_PAY_TO");
540
+ if (!payTo) {
541
+ throw new Error(
542
+ "[serve] T2000_PAY_TO is not set (or is empty). Set it to the Sui address your payments should settle to \u2014 `t2 address` prints yours."
543
+ );
544
+ }
545
+ const network = read("T2000_NETWORK");
546
+ if (network && network !== "mainnet" && network !== "testnet") {
547
+ throw new Error(`[serve] T2000_NETWORK must be 'mainnet' or 'testnet', got "${network}"`);
548
+ }
549
+ const kvUrl = read("KV_REST_API_URL");
550
+ const kvToken = read("KV_REST_API_TOKEN");
551
+ const store = kvUrl && kvToken ? new UpstashDigestStore({ url: kvUrl, token: kvToken }) : void 0;
552
+ if (!store) {
553
+ console.warn(
554
+ "[serve] No KV_REST_API_URL/KV_REST_API_TOKEN \u2014 using the in-memory replay store. Fine for a single long-lived process; on serverless hosts set both so replay protection is durable."
555
+ );
556
+ }
557
+ return new Serve({
558
+ payTo,
559
+ network: network ?? "mainnet",
560
+ baseUrl: read("T2000_BASE_URL"),
561
+ name: read("T2000_NAME"),
562
+ description: read("T2000_DESCRIPTION"),
563
+ store
564
+ });
565
+ }
566
+
567
+ Object.defineProperty(exports, "InMemoryDigestStore", {
568
+ enumerable: true,
569
+ get: function () { return server.InMemoryDigestStore; }
570
+ });
571
+ exports.RouteBuilder = RouteBuilder;
572
+ exports.Serve = Serve;
573
+ exports.UpstashDigestStore = UpstashDigestStore;
574
+ exports.__resetChainCaches = __resetChainCaches;
575
+ exports.__seedChainInfo = __seedChainInfo;
576
+ exports.buildLlmsTxt = buildLlmsTxt;
577
+ exports.buildOpenApiDocument = buildOpenApiDocument;
578
+ exports.createServe = createServe;
579
+ exports.createServeFromEnv = createServeFromEnv;
580
+ //# sourceMappingURL=index.cjs.map
581
+ //# sourceMappingURL=index.cjs.map