@agentcash/router 0.2.2 → 0.3.1
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 +82 -4
- package/dist/index.cjs +78 -8
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +78 -8
- package/package.json +13 -14
package/README.md
CHANGED
|
@@ -18,6 +18,36 @@ pnpm add next zod @x402/core @x402/evm @x402/extensions @coinbase/x402 zod-opena
|
|
|
18
18
|
pnpm add mpay
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Environment Setup
|
|
22
|
+
|
|
23
|
+
The router uses the default facilitator from `@coinbase/x402` for x402 payments, which requires CDP API keys:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
CDP_API_KEY_ID=your-key-id
|
|
27
|
+
CDP_API_KEY_SECRET=your-key-secret
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**For Next.js apps with env validation** (T3 stack, `@t3-oss/env-nextjs`): Add these to your env schema — Next.js doesn't expose undeclared env vars to `process.env`.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// src/env.js
|
|
34
|
+
import { createEnv } from "@t3-oss/env-nextjs";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
export const env = createEnv({
|
|
38
|
+
server: {
|
|
39
|
+
CDP_API_KEY_ID: z.string(),
|
|
40
|
+
CDP_API_KEY_SECRET: z.string(),
|
|
41
|
+
},
|
|
42
|
+
runtimeEnv: {
|
|
43
|
+
CDP_API_KEY_ID: process.env.CDP_API_KEY_ID,
|
|
44
|
+
CDP_API_KEY_SECRET: process.env.CDP_API_KEY_SECRET,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Without these keys, x402 routes will fail to initialize (empty 402 responses, no payment header).
|
|
50
|
+
|
|
21
51
|
## Quick Start
|
|
22
52
|
|
|
23
53
|
### 1. Create the router (once per service)
|
|
@@ -111,11 +141,28 @@ The fluent builder ensures compile-time safety:
|
|
|
111
141
|
|
|
112
142
|
### Pricing Modes
|
|
113
143
|
|
|
114
|
-
**Static
|
|
144
|
+
**Static** - Fixed price for all requests:
|
|
145
|
+
```typescript
|
|
146
|
+
router.route('search').paid('0.02')
|
|
147
|
+
```
|
|
115
148
|
|
|
116
|
-
**Dynamic
|
|
149
|
+
**Dynamic** - Calculate price based on request body:
|
|
150
|
+
```typescript
|
|
151
|
+
router.route('gen')
|
|
152
|
+
.paid((body) => calculateCost(body.imageSize, body.quality))
|
|
153
|
+
.body(imageGenSchema)
|
|
154
|
+
.handler(async ({ body }) => generate(body));
|
|
155
|
+
```
|
|
117
156
|
|
|
118
|
-
**
|
|
157
|
+
**Dynamic with safety net** - Cap at maxPrice if calculation exceeds, fallback to maxPrice on errors:
|
|
158
|
+
```typescript
|
|
159
|
+
router.route('compute')
|
|
160
|
+
.paid((body) => calculateExpensiveOperation(body), { maxPrice: '10.00' })
|
|
161
|
+
.body(computeSchema)
|
|
162
|
+
.handler(async ({ body }) => compute(body));
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Tiered** - Price based on a specific field value:
|
|
119
166
|
```typescript
|
|
120
167
|
router.route('upload').paid({
|
|
121
168
|
field: 'tier',
|
|
@@ -123,7 +170,38 @@ router.route('upload').paid({
|
|
|
123
170
|
'10mb': { price: '0.02', label: '10 MB' },
|
|
124
171
|
'100mb': { price: '0.20', label: '100 MB' },
|
|
125
172
|
},
|
|
126
|
-
}).body(
|
|
173
|
+
}).body(uploadSchema)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### maxPrice Semantics (v0.3.1+)
|
|
177
|
+
|
|
178
|
+
`maxPrice` is **optional** for dynamic pricing and acts as a safety net:
|
|
179
|
+
|
|
180
|
+
1. **Capping**: If `calculateCost(body)` returns `"15.00"` but `maxPrice: "10.00"`, the client is charged `$10.00` (capped) and a warning alert fires.
|
|
181
|
+
|
|
182
|
+
2. **Fallback**: If `calculateCost(body)` throws an error and `maxPrice` is set, the route falls back to `maxPrice` (degraded mode) and an alert fires. Without `maxPrice`, the route returns 500.
|
|
183
|
+
|
|
184
|
+
3. **Trust mode**: No `maxPrice` means full trust in your pricing function (no cap, no fallback).
|
|
185
|
+
|
|
186
|
+
**Best practices:**
|
|
187
|
+
- ✅ Always set `maxPrice` for production routes (safety net)
|
|
188
|
+
- ✅ Use `maxPrice` for routes with external dependencies (pricing APIs)
|
|
189
|
+
- ✅ Monitor alerts for capping events (indicates pricing bug)
|
|
190
|
+
- ⚠️ Skip `maxPrice` only for well-tested, unbounded pricing (e.g., per-GB storage)
|
|
191
|
+
|
|
192
|
+
**Example with safety net:**
|
|
193
|
+
```typescript
|
|
194
|
+
router.route('ai-gen')
|
|
195
|
+
.paid(async (body) => {
|
|
196
|
+
// External pricing API (can fail)
|
|
197
|
+
const res = await fetch('https://pricing.example.com/calculate', {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
body: JSON.stringify(body),
|
|
200
|
+
});
|
|
201
|
+
return res.json().price;
|
|
202
|
+
}, { maxPrice: '5.00' }) // Fallback if API is down
|
|
203
|
+
.body(genSchema)
|
|
204
|
+
.handler(async ({ body }) => generate(body));
|
|
127
205
|
```
|
|
128
206
|
|
|
129
207
|
### Dual Protocol (x402 + MPP)
|
package/dist/index.cjs
CHANGED
|
@@ -523,7 +523,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
523
523
|
if (routeEntry.authMode === "unprotected") {
|
|
524
524
|
return handleAuth(null, void 0);
|
|
525
525
|
}
|
|
526
|
-
let account
|
|
526
|
+
let account;
|
|
527
527
|
if (routeEntry.authMode === "apiKey" || routeEntry.apiKeyResolver) {
|
|
528
528
|
if (!routeEntry.apiKeyResolver) {
|
|
529
529
|
return fail(401, "API key resolver not configured", meta, pluginCtx);
|
|
@@ -538,6 +538,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
540
|
const protocol = detectProtocol(request);
|
|
541
|
+
let earlyBodyData;
|
|
542
|
+
if (!protocol && typeof routeEntry.pricing === "function" && routeEntry.bodySchema) {
|
|
543
|
+
const requestForPricing = request.clone();
|
|
544
|
+
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
545
|
+
if (!earlyBodyResult.ok) {
|
|
546
|
+
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
547
|
+
return earlyBodyResult.response;
|
|
548
|
+
}
|
|
549
|
+
earlyBodyData = earlyBodyResult.data;
|
|
550
|
+
}
|
|
541
551
|
if (routeEntry.authMode === "siwx") {
|
|
542
552
|
if (!request.headers.get("SIGN-IN-WITH-X")) {
|
|
543
553
|
const url = new URL(request.url);
|
|
@@ -606,7 +616,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
606
616
|
return handleAuth(siwx.wallet, void 0);
|
|
607
617
|
}
|
|
608
618
|
if (!protocol || protocol === "siwx") {
|
|
609
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
619
|
+
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
610
620
|
}
|
|
611
621
|
const body = await parseBody(request, routeEntry);
|
|
612
622
|
if (!body.ok) {
|
|
@@ -744,10 +754,55 @@ function parseQuery(request, routeEntry) {
|
|
|
744
754
|
const result = routeEntry.querySchema.safeParse(params);
|
|
745
755
|
return result.success ? result.data : params;
|
|
746
756
|
}
|
|
747
|
-
async function
|
|
757
|
+
async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta) {
|
|
758
|
+
try {
|
|
759
|
+
let price = await resolvePrice(routeEntry.pricing, bodyData);
|
|
760
|
+
if (routeEntry.maxPrice) {
|
|
761
|
+
const calculated = parseFloat(price);
|
|
762
|
+
const max = parseFloat(routeEntry.maxPrice);
|
|
763
|
+
if (calculated > max) {
|
|
764
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
765
|
+
level: "warn",
|
|
766
|
+
message: `Price ${price} exceeds maxPrice ${routeEntry.maxPrice}, capping`,
|
|
767
|
+
route: routeEntry.key,
|
|
768
|
+
meta: { calculated: price, maxPrice: routeEntry.maxPrice, body: bodyData }
|
|
769
|
+
});
|
|
770
|
+
price = routeEntry.maxPrice;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return { price };
|
|
774
|
+
} catch (err) {
|
|
775
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
776
|
+
level: "error",
|
|
777
|
+
message: `Pricing function failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
778
|
+
route: routeEntry.key,
|
|
779
|
+
meta: { error: err instanceof Error ? err.stack : String(err), body: bodyData }
|
|
780
|
+
});
|
|
781
|
+
if (routeEntry.maxPrice) {
|
|
782
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
783
|
+
level: "warn",
|
|
784
|
+
message: `Using maxPrice ${routeEntry.maxPrice} as fallback after pricing error`,
|
|
785
|
+
route: routeEntry.key
|
|
786
|
+
});
|
|
787
|
+
return { price: routeEntry.maxPrice };
|
|
788
|
+
} else {
|
|
789
|
+
const errorResponse = import_server2.NextResponse.json(
|
|
790
|
+
{ success: false, error: "Price calculation failed" },
|
|
791
|
+
{ status: 500 }
|
|
792
|
+
);
|
|
793
|
+
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
794
|
+
return { error: errorResponse };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
748
799
|
const response = new import_server2.NextResponse(null, { status: 402 });
|
|
749
800
|
let challengePrice;
|
|
750
|
-
if (routeEntry.
|
|
801
|
+
if (bodyData !== void 0 && typeof routeEntry.pricing === "function") {
|
|
802
|
+
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
803
|
+
if ("error" in result) return result.error;
|
|
804
|
+
challengePrice = result.price;
|
|
805
|
+
} else if (routeEntry.maxPrice) {
|
|
751
806
|
challengePrice = routeEntry.maxPrice;
|
|
752
807
|
} else if (routeEntry.pricing) {
|
|
753
808
|
try {
|
|
@@ -908,9 +963,6 @@ var RouteBuilder = class {
|
|
|
908
963
|
next._pricing = pricing;
|
|
909
964
|
if (options?.protocols) next._protocols = options.protocols;
|
|
910
965
|
if (options?.maxPrice) next._maxPrice = options.maxPrice;
|
|
911
|
-
if (typeof pricing === "function" && !options?.maxPrice) {
|
|
912
|
-
throw new Error(`route '${this._key}': dynamic pricing requires maxPrice option`);
|
|
913
|
-
}
|
|
914
966
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
915
967
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
916
968
|
if (!tierKey) {
|
|
@@ -1178,6 +1230,23 @@ function createRouter(config) {
|
|
|
1178
1230
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1179
1231
|
const network = config.network ?? "eip155:8453";
|
|
1180
1232
|
const baseUrl = typeof globalThis.process !== "undefined" ? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" : "http://localhost:3000";
|
|
1233
|
+
if (config.protocols) {
|
|
1234
|
+
if (config.protocols.length === 0) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
"RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (config.protocols.includes("mpp") && !config.mpp) {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
'RouterConfig.protocols includes "mpp" but RouterConfig.mpp is not configured. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (config.protocols.includes("x402") && !config.payeeAddress) {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
'RouterConfig.protocols includes "x402" but RouterConfig.payeeAddress is not configured.'
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1181
1250
|
if (config.plugin?.init) {
|
|
1182
1251
|
try {
|
|
1183
1252
|
const result = config.plugin.init({ origin: baseUrl });
|
|
@@ -1213,7 +1282,8 @@ function createRouter(config) {
|
|
|
1213
1282
|
route(key) {
|
|
1214
1283
|
const builder = new RouteBuilder(key, registry, deps);
|
|
1215
1284
|
if (config.prices && key in config.prices) {
|
|
1216
|
-
|
|
1285
|
+
const options = config.protocols ? { protocols: config.protocols } : void 0;
|
|
1286
|
+
return builder.paid(config.prices[key], options);
|
|
1217
1287
|
}
|
|
1218
1288
|
return builder;
|
|
1219
1289
|
},
|
package/dist/index.d.cts
CHANGED
|
@@ -184,6 +184,20 @@ interface RouterConfig {
|
|
|
184
184
|
currency: string;
|
|
185
185
|
recipient?: string;
|
|
186
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* Payment protocols to accept on auto-priced routes (those using the `prices` config).
|
|
189
|
+
*
|
|
190
|
+
* @default ['x402']
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* // Accept both x402 and MPP payments
|
|
194
|
+
* createRouter({
|
|
195
|
+
* protocols: ['x402', 'mpp'],
|
|
196
|
+
* mpp: { secretKey, currency, recipient },
|
|
197
|
+
* prices: { 'exa/search': '0.01' }
|
|
198
|
+
* })
|
|
199
|
+
*/
|
|
200
|
+
protocols?: ProtocolType[];
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
declare class RouteRegistry {
|
|
@@ -251,7 +265,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
251
265
|
private fork;
|
|
252
266
|
paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, False, HasBody>;
|
|
253
267
|
paid<TBodyIn>(pricing: (body: TBodyIn) => string | Promise<string>, options?: PaidOptions & {
|
|
254
|
-
maxPrice
|
|
268
|
+
maxPrice?: string;
|
|
255
269
|
}): RouteBuilder<TBody, TQuery, True, True, HasBody>;
|
|
256
270
|
paid(pricing: {
|
|
257
271
|
field: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -184,6 +184,20 @@ interface RouterConfig {
|
|
|
184
184
|
currency: string;
|
|
185
185
|
recipient?: string;
|
|
186
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* Payment protocols to accept on auto-priced routes (those using the `prices` config).
|
|
189
|
+
*
|
|
190
|
+
* @default ['x402']
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* // Accept both x402 and MPP payments
|
|
194
|
+
* createRouter({
|
|
195
|
+
* protocols: ['x402', 'mpp'],
|
|
196
|
+
* mpp: { secretKey, currency, recipient },
|
|
197
|
+
* prices: { 'exa/search': '0.01' }
|
|
198
|
+
* })
|
|
199
|
+
*/
|
|
200
|
+
protocols?: ProtocolType[];
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
declare class RouteRegistry {
|
|
@@ -251,7 +265,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
251
265
|
private fork;
|
|
252
266
|
paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, False, HasBody>;
|
|
253
267
|
paid<TBodyIn>(pricing: (body: TBodyIn) => string | Promise<string>, options?: PaidOptions & {
|
|
254
|
-
maxPrice
|
|
268
|
+
maxPrice?: string;
|
|
255
269
|
}): RouteBuilder<TBody, TQuery, True, True, HasBody>;
|
|
256
270
|
paid(pricing: {
|
|
257
271
|
field: string;
|
package/dist/index.js
CHANGED
|
@@ -489,7 +489,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
489
489
|
if (routeEntry.authMode === "unprotected") {
|
|
490
490
|
return handleAuth(null, void 0);
|
|
491
491
|
}
|
|
492
|
-
let account
|
|
492
|
+
let account;
|
|
493
493
|
if (routeEntry.authMode === "apiKey" || routeEntry.apiKeyResolver) {
|
|
494
494
|
if (!routeEntry.apiKeyResolver) {
|
|
495
495
|
return fail(401, "API key resolver not configured", meta, pluginCtx);
|
|
@@ -504,6 +504,16 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
506
|
const protocol = detectProtocol(request);
|
|
507
|
+
let earlyBodyData;
|
|
508
|
+
if (!protocol && typeof routeEntry.pricing === "function" && routeEntry.bodySchema) {
|
|
509
|
+
const requestForPricing = request.clone();
|
|
510
|
+
const earlyBodyResult = await parseBody(requestForPricing, routeEntry);
|
|
511
|
+
if (!earlyBodyResult.ok) {
|
|
512
|
+
firePluginResponse(deps, pluginCtx, meta, earlyBodyResult.response);
|
|
513
|
+
return earlyBodyResult.response;
|
|
514
|
+
}
|
|
515
|
+
earlyBodyData = earlyBodyResult.data;
|
|
516
|
+
}
|
|
507
517
|
if (routeEntry.authMode === "siwx") {
|
|
508
518
|
if (!request.headers.get("SIGN-IN-WITH-X")) {
|
|
509
519
|
const url = new URL(request.url);
|
|
@@ -572,7 +582,7 @@ function createRequestHandler(routeEntry, handler, deps) {
|
|
|
572
582
|
return handleAuth(siwx.wallet, void 0);
|
|
573
583
|
}
|
|
574
584
|
if (!protocol || protocol === "siwx") {
|
|
575
|
-
return await build402(request, routeEntry, deps, meta, pluginCtx);
|
|
585
|
+
return await build402(request, routeEntry, deps, meta, pluginCtx, earlyBodyData);
|
|
576
586
|
}
|
|
577
587
|
const body = await parseBody(request, routeEntry);
|
|
578
588
|
if (!body.ok) {
|
|
@@ -710,10 +720,55 @@ function parseQuery(request, routeEntry) {
|
|
|
710
720
|
const result = routeEntry.querySchema.safeParse(params);
|
|
711
721
|
return result.success ? result.data : params;
|
|
712
722
|
}
|
|
713
|
-
async function
|
|
723
|
+
async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta) {
|
|
724
|
+
try {
|
|
725
|
+
let price = await resolvePrice(routeEntry.pricing, bodyData);
|
|
726
|
+
if (routeEntry.maxPrice) {
|
|
727
|
+
const calculated = parseFloat(price);
|
|
728
|
+
const max = parseFloat(routeEntry.maxPrice);
|
|
729
|
+
if (calculated > max) {
|
|
730
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
731
|
+
level: "warn",
|
|
732
|
+
message: `Price ${price} exceeds maxPrice ${routeEntry.maxPrice}, capping`,
|
|
733
|
+
route: routeEntry.key,
|
|
734
|
+
meta: { calculated: price, maxPrice: routeEntry.maxPrice, body: bodyData }
|
|
735
|
+
});
|
|
736
|
+
price = routeEntry.maxPrice;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return { price };
|
|
740
|
+
} catch (err) {
|
|
741
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
742
|
+
level: "error",
|
|
743
|
+
message: `Pricing function failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
744
|
+
route: routeEntry.key,
|
|
745
|
+
meta: { error: err instanceof Error ? err.stack : String(err), body: bodyData }
|
|
746
|
+
});
|
|
747
|
+
if (routeEntry.maxPrice) {
|
|
748
|
+
firePluginHook(deps.plugin, "onAlert", pluginCtx, {
|
|
749
|
+
level: "warn",
|
|
750
|
+
message: `Using maxPrice ${routeEntry.maxPrice} as fallback after pricing error`,
|
|
751
|
+
route: routeEntry.key
|
|
752
|
+
});
|
|
753
|
+
return { price: routeEntry.maxPrice };
|
|
754
|
+
} else {
|
|
755
|
+
const errorResponse = NextResponse2.json(
|
|
756
|
+
{ success: false, error: "Price calculation failed" },
|
|
757
|
+
{ status: 500 }
|
|
758
|
+
);
|
|
759
|
+
firePluginResponse(deps, pluginCtx, meta, errorResponse);
|
|
760
|
+
return { error: errorResponse };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
|
|
714
765
|
const response = new NextResponse2(null, { status: 402 });
|
|
715
766
|
let challengePrice;
|
|
716
|
-
if (routeEntry.
|
|
767
|
+
if (bodyData !== void 0 && typeof routeEntry.pricing === "function") {
|
|
768
|
+
const result = await resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta);
|
|
769
|
+
if ("error" in result) return result.error;
|
|
770
|
+
challengePrice = result.price;
|
|
771
|
+
} else if (routeEntry.maxPrice) {
|
|
717
772
|
challengePrice = routeEntry.maxPrice;
|
|
718
773
|
} else if (routeEntry.pricing) {
|
|
719
774
|
try {
|
|
@@ -874,9 +929,6 @@ var RouteBuilder = class {
|
|
|
874
929
|
next._pricing = pricing;
|
|
875
930
|
if (options?.protocols) next._protocols = options.protocols;
|
|
876
931
|
if (options?.maxPrice) next._maxPrice = options.maxPrice;
|
|
877
|
-
if (typeof pricing === "function" && !options?.maxPrice) {
|
|
878
|
-
throw new Error(`route '${this._key}': dynamic pricing requires maxPrice option`);
|
|
879
|
-
}
|
|
880
932
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
881
933
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
882
934
|
if (!tierKey) {
|
|
@@ -1144,6 +1196,23 @@ function createRouter(config) {
|
|
|
1144
1196
|
const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
|
|
1145
1197
|
const network = config.network ?? "eip155:8453";
|
|
1146
1198
|
const baseUrl = typeof globalThis.process !== "undefined" ? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" : "http://localhost:3000";
|
|
1199
|
+
if (config.protocols) {
|
|
1200
|
+
if (config.protocols.length === 0) {
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
"RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
if (config.protocols.includes("mpp") && !config.mpp) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
'RouterConfig.protocols includes "mpp" but RouterConfig.mpp is not configured. Add mpp: { secretKey, currency, recipient } to your router config.'
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
if (config.protocols.includes("x402") && !config.payeeAddress) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
'RouterConfig.protocols includes "x402" but RouterConfig.payeeAddress is not configured.'
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1147
1216
|
if (config.plugin?.init) {
|
|
1148
1217
|
try {
|
|
1149
1218
|
const result = config.plugin.init({ origin: baseUrl });
|
|
@@ -1179,7 +1248,8 @@ function createRouter(config) {
|
|
|
1179
1248
|
route(key) {
|
|
1180
1249
|
const builder = new RouteBuilder(key, registry, deps);
|
|
1181
1250
|
if (config.prices && key in config.prices) {
|
|
1182
|
-
|
|
1251
|
+
const options = config.protocols ? { protocols: config.protocols } : void 0;
|
|
1252
|
+
return builder.paid(config.prices[key], options);
|
|
1183
1253
|
}
|
|
1184
1254
|
return builder;
|
|
1185
1255
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,17 +21,6 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"build": "tsup",
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"lint": "eslint src/",
|
|
28
|
-
"lint:fix": "eslint src/ --fix",
|
|
29
|
-
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
30
|
-
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
31
|
-
"test": "vitest run",
|
|
32
|
-
"test:watch": "vitest",
|
|
33
|
-
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm build && pnpm test"
|
|
34
|
-
},
|
|
35
24
|
"peerDependencies": {
|
|
36
25
|
"@coinbase/x402": "^2.1.0",
|
|
37
26
|
"@x402/core": "^2.3.0",
|
|
@@ -66,10 +55,20 @@
|
|
|
66
55
|
"zod": "^4.0.0",
|
|
67
56
|
"zod-openapi": "^5.0.0"
|
|
68
57
|
},
|
|
69
|
-
"packageManager": "pnpm@10.28.0",
|
|
70
58
|
"license": "MIT",
|
|
71
59
|
"repository": {
|
|
72
60
|
"type": "git",
|
|
73
61
|
"url": "git+https://github.com/merit-systems/agentcash-router.git"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsup",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"lint": "eslint src/",
|
|
67
|
+
"lint:fix": "eslint src/ --fix",
|
|
68
|
+
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
69
|
+
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"test:watch": "vitest",
|
|
72
|
+
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm build && pnpm test"
|
|
74
73
|
}
|
|
75
|
-
}
|
|
74
|
+
}
|