@agentcash/router 0.3.0 → 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 +52 -4
- package/dist/index.cjs +59 -7
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -7
- package/package.json +13 -14
package/README.md
CHANGED
|
@@ -141,11 +141,28 @@ The fluent builder ensures compile-time safety:
|
|
|
141
141
|
|
|
142
142
|
### Pricing Modes
|
|
143
143
|
|
|
144
|
-
**Static
|
|
144
|
+
**Static** - Fixed price for all requests:
|
|
145
|
+
```typescript
|
|
146
|
+
router.route('search').paid('0.02')
|
|
147
|
+
```
|
|
148
|
+
|
|
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
|
+
```
|
|
145
156
|
|
|
146
|
-
**Dynamic
|
|
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
|
+
```
|
|
147
164
|
|
|
148
|
-
**Tiered
|
|
165
|
+
**Tiered** - Price based on a specific field value:
|
|
149
166
|
```typescript
|
|
150
167
|
router.route('upload').paid({
|
|
151
168
|
field: 'tier',
|
|
@@ -153,7 +170,38 @@ router.route('upload').paid({
|
|
|
153
170
|
'10mb': { price: '0.02', label: '10 MB' },
|
|
154
171
|
'100mb': { price: '0.20', label: '100 MB' },
|
|
155
172
|
},
|
|
156
|
-
}).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));
|
|
157
205
|
```
|
|
158
206
|
|
|
159
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) {
|
package/dist/index.d.cts
CHANGED
|
@@ -265,7 +265,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
265
265
|
private fork;
|
|
266
266
|
paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, False, HasBody>;
|
|
267
267
|
paid<TBodyIn>(pricing: (body: TBodyIn) => string | Promise<string>, options?: PaidOptions & {
|
|
268
|
-
maxPrice
|
|
268
|
+
maxPrice?: string;
|
|
269
269
|
}): RouteBuilder<TBody, TQuery, True, True, HasBody>;
|
|
270
270
|
paid(pricing: {
|
|
271
271
|
field: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -265,7 +265,7 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, HasAuth extend
|
|
|
265
265
|
private fork;
|
|
266
266
|
paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, True, False, HasBody>;
|
|
267
267
|
paid<TBodyIn>(pricing: (body: TBodyIn) => string | Promise<string>, options?: PaidOptions & {
|
|
268
|
-
maxPrice
|
|
268
|
+
maxPrice?: string;
|
|
269
269
|
}): RouteBuilder<TBody, TQuery, True, True, HasBody>;
|
|
270
270
|
paid(pricing: {
|
|
271
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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "0.3.
|
|
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
|
+
}
|