@agentcash/router 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.cjs +289 -115
- package/dist/index.d.cts +106 -85
- package/dist/index.d.ts +106 -85
- package/dist/index.js +287 -113
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -45,6 +45,9 @@ function getConfiguredX402Accepts(config) {
|
|
|
45
45
|
function getConfiguredX402Networks(config) {
|
|
46
46
|
return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
|
|
47
47
|
}
|
|
48
|
+
function selectRouteAccepts(accepts, routeEntry) {
|
|
49
|
+
return routeEntry.billing === "upto" ? accepts.filter((accept) => accept.scheme === "upto") : accepts.filter((accept) => (accept.scheme ?? "exact") !== "upto");
|
|
50
|
+
}
|
|
48
51
|
async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
|
|
49
52
|
return Promise.all(
|
|
50
53
|
accepts.map(async (accept) => ({
|
|
@@ -340,9 +343,18 @@ function withScopedKinds(client, kinds) {
|
|
|
340
343
|
return {
|
|
341
344
|
verify: client.verify.bind(client),
|
|
342
345
|
settle: client.settle.bind(client),
|
|
343
|
-
getSupported: async () =>
|
|
346
|
+
getSupported: async () => {
|
|
347
|
+
const live = await client.getSupported();
|
|
348
|
+
return { ...live, kinds: mergeKindExtras(kinds, live.kinds) };
|
|
349
|
+
}
|
|
344
350
|
};
|
|
345
351
|
}
|
|
352
|
+
function mergeKindExtras(scoped, live) {
|
|
353
|
+
return scoped.map((kind) => {
|
|
354
|
+
const match = live.find((l) => l.scheme === kind.scheme && l.network === kind.network);
|
|
355
|
+
return match?.extra ? { ...kind, extra: { ...kind.extra, ...match.extra } } : kind;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
346
358
|
function buildSupportedKinds(group) {
|
|
347
359
|
return group.networks.flatMap((network) => {
|
|
348
360
|
if (group.family === "solana") {
|
|
@@ -754,10 +766,7 @@ async function runHandler(ctx, handlerCtx) {
|
|
|
754
766
|
}
|
|
755
767
|
if (isAsyncIterable(returned) && !isThenable(returned)) {
|
|
756
768
|
return errorResult(
|
|
757
|
-
new HttpError(
|
|
758
|
-
`route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
|
|
759
|
-
500
|
|
760
|
-
)
|
|
769
|
+
new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
|
|
761
770
|
);
|
|
762
771
|
}
|
|
763
772
|
let rawResult;
|
|
@@ -1190,6 +1199,7 @@ var DynamicPricing = class {
|
|
|
1190
1199
|
const raw = await this.opts.fn(body);
|
|
1191
1200
|
return this.cap(raw, body);
|
|
1192
1201
|
} catch (err) {
|
|
1202
|
+
if (err instanceof HttpError) throw err;
|
|
1193
1203
|
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
1194
1204
|
error: err instanceof Error ? err.stack : String(err),
|
|
1195
1205
|
body
|
|
@@ -1712,7 +1722,7 @@ var mppStrategy = {
|
|
|
1712
1722
|
async verify(args) {
|
|
1713
1723
|
const info = readMppCredential(args.request);
|
|
1714
1724
|
if (!info) return { ok: false, kind: "invalid" };
|
|
1715
|
-
if (args.routeEntry.
|
|
1725
|
+
if (args.routeEntry.billing === "metered") {
|
|
1716
1726
|
if (!info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1717
1727
|
return verifySessionMode(args, info);
|
|
1718
1728
|
}
|
|
@@ -1763,7 +1773,7 @@ var mppStrategy = {
|
|
|
1763
1773
|
async buildChallenge(args) {
|
|
1764
1774
|
if (!args.deps.mppx) return {};
|
|
1765
1775
|
const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
|
|
1766
|
-
if (args.routeEntry.
|
|
1776
|
+
if (args.routeEntry.billing === "metered" && sessionsConfigured) {
|
|
1767
1777
|
const tickCost = args.routeEntry.tickCost;
|
|
1768
1778
|
const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
|
|
1769
1779
|
const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
|
|
@@ -2140,7 +2150,7 @@ async function verifyX402(args) {
|
|
|
2140
2150
|
const accepts = await resolveX402Accepts(
|
|
2141
2151
|
request,
|
|
2142
2152
|
routeEntry,
|
|
2143
|
-
deps.x402Accepts,
|
|
2153
|
+
selectRouteAccepts(deps.x402Accepts, routeEntry),
|
|
2144
2154
|
deps.payeeAddress,
|
|
2145
2155
|
body
|
|
2146
2156
|
);
|
|
@@ -2188,7 +2198,7 @@ async function verifyX402(args) {
|
|
|
2188
2198
|
async function settleX402(args) {
|
|
2189
2199
|
const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
|
|
2190
2200
|
const { payload, requirements } = token;
|
|
2191
|
-
const override = routeEntry.
|
|
2201
|
+
const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
|
|
2192
2202
|
try {
|
|
2193
2203
|
const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
|
|
2194
2204
|
if (!settle.result?.success) {
|
|
@@ -2218,7 +2228,7 @@ async function buildX402ChallengeContribution(args) {
|
|
|
2218
2228
|
const accepts = await resolveX402Accepts(
|
|
2219
2229
|
request,
|
|
2220
2230
|
routeEntry,
|
|
2221
|
-
deps.x402Accepts,
|
|
2231
|
+
selectRouteAccepts(deps.x402Accepts, routeEntry),
|
|
2222
2232
|
deps.payeeAddress,
|
|
2223
2233
|
body
|
|
2224
2234
|
);
|
|
@@ -2311,9 +2321,7 @@ async function buildChallengeExtensions(ctx) {
|
|
|
2311
2321
|
} catch {
|
|
2312
2322
|
}
|
|
2313
2323
|
}
|
|
2314
|
-
const hasEvmUpto = ctx.deps.x402Accepts.some(
|
|
2315
|
-
(accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
|
|
2316
|
-
);
|
|
2324
|
+
const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
|
|
2317
2325
|
if (hasEvmUpto) {
|
|
2318
2326
|
try {
|
|
2319
2327
|
const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
|
|
@@ -2480,10 +2488,7 @@ async function runDynamicChannelMgmtFlow(args) {
|
|
|
2480
2488
|
});
|
|
2481
2489
|
}
|
|
2482
2490
|
|
|
2483
|
-
// src/
|
|
2484
|
-
import { NextResponse as NextResponse6 } from "next/server";
|
|
2485
|
-
|
|
2486
|
-
// src/pricing/charge-context.ts
|
|
2491
|
+
// src/pricing/metered-charge.ts
|
|
2487
2492
|
function createChargeContext(args) {
|
|
2488
2493
|
const { tickCost, maxPrice, route } = args;
|
|
2489
2494
|
const tickAtomic = decimalToAtomic(tickCost);
|
|
@@ -2518,15 +2523,10 @@ function createChargeContext(args) {
|
|
|
2518
2523
|
};
|
|
2519
2524
|
}
|
|
2520
2525
|
|
|
2521
|
-
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
tickCost: ctx.routeEntry.tickCost,
|
|
2526
|
-
maxPrice: ctx.routeEntry.maxPrice,
|
|
2527
|
-
route: ctx.routeEntry.key
|
|
2528
|
-
}) : null;
|
|
2529
|
-
const baseHandlerCtx = {
|
|
2526
|
+
// src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
|
|
2527
|
+
import { NextResponse as NextResponse6 } from "next/server";
|
|
2528
|
+
function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
|
|
2529
|
+
return {
|
|
2530
2530
|
body,
|
|
2531
2531
|
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
2532
2532
|
request: ctx.request,
|
|
@@ -2538,12 +2538,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
|
2538
2538
|
alert: ctx.report,
|
|
2539
2539
|
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
2540
2540
|
};
|
|
2541
|
+
}
|
|
2542
|
+
function toResponse(rawResult) {
|
|
2543
|
+
return rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
|
|
2544
|
+
}
|
|
2545
|
+
function errorResult2(error) {
|
|
2546
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
2547
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
2548
|
+
return {
|
|
2549
|
+
kind: "request",
|
|
2550
|
+
response: NextResponse6.json({ success: false, error: message }, { status }),
|
|
2551
|
+
rawResult: void 0,
|
|
2552
|
+
handlerError: error
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
function isAsyncIterable2(value) {
|
|
2556
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
2557
|
+
}
|
|
2558
|
+
function isThenable2(value) {
|
|
2559
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
|
|
2563
|
+
async function invokeMetered(ctx, wallet, account, body, payment) {
|
|
2564
|
+
const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
|
|
2565
|
+
tickCost: ctx.routeEntry.tickCost,
|
|
2566
|
+
maxPrice: ctx.routeEntry.maxPrice,
|
|
2567
|
+
route: ctx.routeEntry.key
|
|
2568
|
+
}) : null;
|
|
2569
|
+
const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
|
|
2541
2570
|
const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
|
|
2542
2571
|
let returned;
|
|
2543
2572
|
try {
|
|
2544
2573
|
returned = ctx.handler(handlerCtx);
|
|
2545
2574
|
} catch (error) {
|
|
2546
|
-
return errorResult2(error
|
|
2575
|
+
return errorResult2(error);
|
|
2547
2576
|
}
|
|
2548
2577
|
if (isAsyncIterable2(returned) && !isThenable2(returned)) {
|
|
2549
2578
|
if (!chargeContext) {
|
|
@@ -2551,41 +2580,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
|
2551
2580
|
new HttpError(
|
|
2552
2581
|
"route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
|
|
2553
2582
|
500
|
|
2554
|
-
)
|
|
2555
|
-
null
|
|
2583
|
+
)
|
|
2556
2584
|
);
|
|
2557
2585
|
}
|
|
2558
|
-
return {
|
|
2559
|
-
kind: "stream",
|
|
2560
|
-
source: returned,
|
|
2561
|
-
chargeContext
|
|
2562
|
-
};
|
|
2586
|
+
return { kind: "stream", source: returned, chargeContext };
|
|
2563
2587
|
}
|
|
2564
2588
|
let rawResult;
|
|
2565
2589
|
try {
|
|
2566
2590
|
rawResult = await returned;
|
|
2567
2591
|
} catch (error) {
|
|
2568
|
-
return errorResult2(error
|
|
2592
|
+
return errorResult2(error);
|
|
2569
2593
|
}
|
|
2570
|
-
|
|
2571
|
-
return { kind: "request", response, rawResult };
|
|
2594
|
+
return { kind: "request", response: toResponse(rawResult), rawResult };
|
|
2572
2595
|
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2596
|
+
|
|
2597
|
+
// src/pricing/upto-charge.ts
|
|
2598
|
+
function createUptoChargeContext(args) {
|
|
2599
|
+
const { maxPrice, route } = args;
|
|
2600
|
+
const capAtomic = decimalToAtomic(maxPrice);
|
|
2601
|
+
if (capAtomic <= 0n) {
|
|
2602
|
+
throw new Error(`route '${route}': maxPrice '${maxPrice}' must be a positive decimal string`);
|
|
2603
|
+
}
|
|
2604
|
+
let calls = 0;
|
|
2605
|
+
let atomic = 0n;
|
|
2606
|
+
const charge = async (amount) => {
|
|
2607
|
+
const nextAtomic = atomic + decimalToAtomic(amount);
|
|
2608
|
+
if (nextAtomic > capAtomic) {
|
|
2609
|
+
throw Object.assign(
|
|
2610
|
+
new Error(
|
|
2611
|
+
`route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
|
|
2612
|
+
),
|
|
2613
|
+
{ status: 400, code: "CHARGE_OVER_CAP" }
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
calls += 1;
|
|
2617
|
+
atomic = nextAtomic;
|
|
2618
|
+
};
|
|
2577
2619
|
return {
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
handlerError: error
|
|
2620
|
+
charge,
|
|
2621
|
+
callCount: () => calls,
|
|
2622
|
+
atomicTotal: () => atomic
|
|
2582
2623
|
};
|
|
2583
2624
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2625
|
+
|
|
2626
|
+
// src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
|
|
2627
|
+
async function invokeUpto(ctx, wallet, account, body, payment) {
|
|
2628
|
+
const uptoCtx = createUptoChargeContext({
|
|
2629
|
+
maxPrice: ctx.routeEntry.maxPrice,
|
|
2630
|
+
route: ctx.routeEntry.key
|
|
2631
|
+
});
|
|
2632
|
+
const handlerCtx = {
|
|
2633
|
+
...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
|
|
2634
|
+
charge: uptoCtx.charge
|
|
2635
|
+
};
|
|
2636
|
+
let returned;
|
|
2637
|
+
try {
|
|
2638
|
+
returned = ctx.handler(handlerCtx);
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
return errorResult2(error);
|
|
2641
|
+
}
|
|
2642
|
+
if (isAsyncIterable2(returned) && !isThenable2(returned)) {
|
|
2643
|
+
return errorResult2(
|
|
2644
|
+
new HttpError(
|
|
2645
|
+
"streaming is not supported on .upTo() routes \u2014 return a value from .handler() instead",
|
|
2646
|
+
500
|
|
2647
|
+
)
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
let rawResult;
|
|
2651
|
+
try {
|
|
2652
|
+
rawResult = await returned;
|
|
2653
|
+
} catch (error) {
|
|
2654
|
+
return errorResult2(error);
|
|
2655
|
+
}
|
|
2656
|
+
return { kind: "request", response: toResponse(rawResult), rawResult, uptoContext: uptoCtx };
|
|
2589
2657
|
}
|
|
2590
2658
|
|
|
2591
2659
|
// src/pipeline/flows/dynamic/dynamic-preflight.ts
|
|
@@ -2615,7 +2683,7 @@ async function runDynamicRequestFlow(args) {
|
|
|
2615
2683
|
}
|
|
2616
2684
|
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2617
2685
|
if (beforeErr) return beforeErr;
|
|
2618
|
-
const billedAmount = routeEntry
|
|
2686
|
+
const billedAmount = computeBilledAmount(routeEntry, result);
|
|
2619
2687
|
return settleAndFinalizeRequest({
|
|
2620
2688
|
ctx,
|
|
2621
2689
|
strategy,
|
|
@@ -2633,6 +2701,19 @@ async function runDynamicRequestFlow(args) {
|
|
|
2633
2701
|
}
|
|
2634
2702
|
});
|
|
2635
2703
|
}
|
|
2704
|
+
function computeBilledAmount(routeEntry, result) {
|
|
2705
|
+
if (routeEntry.billing === "upto") {
|
|
2706
|
+
const total = result.uptoContext?.atomicTotal() ?? 0n;
|
|
2707
|
+
if (total <= 0n) {
|
|
2708
|
+
throw new HttpError(
|
|
2709
|
+
`route '${routeEntry.key}': handler did not call charge(amount) \u2014 upto routes must accumulate a non-zero billed amount`,
|
|
2710
|
+
500
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
return atomicToDecimal(total);
|
|
2714
|
+
}
|
|
2715
|
+
return routeEntry.tickCost;
|
|
2716
|
+
}
|
|
2636
2717
|
|
|
2637
2718
|
// src/pipeline/flows/dynamic/dynamic-stream.ts
|
|
2638
2719
|
async function runDynamicStreamFlow(args) {
|
|
@@ -2705,13 +2786,7 @@ async function runDynamicPaidFlow(ctx) {
|
|
|
2705
2786
|
amount: price,
|
|
2706
2787
|
network: verifyOutcome.payment.network
|
|
2707
2788
|
});
|
|
2708
|
-
const result = await invokeDynamic(
|
|
2709
|
-
ctx,
|
|
2710
|
-
verifyOutcome.wallet,
|
|
2711
|
-
account,
|
|
2712
|
-
parsedBody,
|
|
2713
|
-
verifyOutcome.payment
|
|
2714
|
-
);
|
|
2789
|
+
const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
|
|
2715
2790
|
switch (result.kind) {
|
|
2716
2791
|
case "stream":
|
|
2717
2792
|
return runDynamicStreamFlow({
|
|
@@ -2733,6 +2808,18 @@ async function runDynamicPaidFlow(ctx) {
|
|
|
2733
2808
|
});
|
|
2734
2809
|
}
|
|
2735
2810
|
}
|
|
2811
|
+
async function invokeDynamic(ctx, verifyOutcome, account, parsedBody) {
|
|
2812
|
+
switch (ctx.routeEntry.billing) {
|
|
2813
|
+
case "upto":
|
|
2814
|
+
return invokeUpto(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
|
|
2815
|
+
case "metered":
|
|
2816
|
+
return invokeMetered(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
|
|
2817
|
+
case "exact":
|
|
2818
|
+
throw new Error(
|
|
2819
|
+
`route '${ctx.routeEntry.key}': exact billing must not reach the dynamic paid flow`
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2736
2823
|
|
|
2737
2824
|
// src/pipeline/flows/static/static-body-and-price.ts
|
|
2738
2825
|
async function resolveStaticBodyAndPrice(args) {
|
|
@@ -2880,13 +2967,8 @@ async function runStaticPaidFlow(ctx) {
|
|
|
2880
2967
|
|
|
2881
2968
|
// src/pipeline/flows/paid.ts
|
|
2882
2969
|
async function runPaidFlow(ctx) {
|
|
2883
|
-
const
|
|
2884
|
-
|
|
2885
|
-
case true:
|
|
2886
|
-
return runDynamicPaidFlow(ctx);
|
|
2887
|
-
case false:
|
|
2888
|
-
return runStaticPaidFlow(ctx);
|
|
2889
|
-
}
|
|
2970
|
+
const handlerCharged = ctx.routeEntry.billing !== "exact";
|
|
2971
|
+
return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
|
|
2890
2972
|
}
|
|
2891
2973
|
|
|
2892
2974
|
// src/pipeline/flows/siwx-only.ts
|
|
@@ -3284,7 +3366,7 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3284
3366
|
protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
|
|
3285
3367
|
maxPrice: void 0,
|
|
3286
3368
|
minPrice: void 0,
|
|
3287
|
-
|
|
3369
|
+
billing: "exact",
|
|
3288
3370
|
tickCost: void 0,
|
|
3289
3371
|
unitType: void 0,
|
|
3290
3372
|
payTo: void 0,
|
|
@@ -3315,39 +3397,84 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3315
3397
|
next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
|
|
3316
3398
|
return next;
|
|
3317
3399
|
}
|
|
3318
|
-
paid(
|
|
3319
|
-
|
|
3400
|
+
paid(arg, options) {
|
|
3401
|
+
return this.applyPaid(normalizePaidArg(this.#s.key, arg, options), "paid");
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* x402-only handler-computed billing. The handler receives `charge(amount)`
|
|
3405
|
+
* and the request settles once for the accumulated total, capped at
|
|
3406
|
+
* `maxPrice`. Requires an `'upto'` accept on at least one configured network.
|
|
3407
|
+
* Pass a bare string as sugar for `{ maxPrice }`.
|
|
3408
|
+
*
|
|
3409
|
+
* @example
|
|
3410
|
+
* ```ts
|
|
3411
|
+
* router.route('llm')
|
|
3412
|
+
* .upTo('0.05')
|
|
3413
|
+
* .body(schema)
|
|
3414
|
+
* .handler(async ({ body, charge }) => { await charge('0.001'); ... });
|
|
3415
|
+
* ```
|
|
3416
|
+
*/
|
|
3417
|
+
upTo(arg) {
|
|
3418
|
+
return this.applyPaid(normalizeUpToArg(this.#s.key, arg), "upTo");
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* MPP-only per-tick billing. `.handler()` bills exactly `tickCost`;
|
|
3422
|
+
* `.stream()` calls `charge()` (no-arg) per yield, settling per tick up to
|
|
3423
|
+
* `maxPrice`. Requires `RouterConfig.mpp.session`.
|
|
3424
|
+
*
|
|
3425
|
+
* @example
|
|
3426
|
+
* ```ts
|
|
3427
|
+
* router.route('llm/stream')
|
|
3428
|
+
* .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
|
|
3429
|
+
* .stream(async function* ({ charge }) { await charge(); yield 'hi'; });
|
|
3430
|
+
* ```
|
|
3431
|
+
*/
|
|
3432
|
+
metered(options) {
|
|
3433
|
+
return this.applyPaid(normalizeMeteredArg(this.#s.key, options), "metered");
|
|
3434
|
+
}
|
|
3435
|
+
applyPaid(normalized, method) {
|
|
3436
|
+
const { pricing, resolvedOptions, billing, tickCost, unitType, maxPrice } = normalized;
|
|
3320
3437
|
if (this.#s.authMode === "unprotected") {
|
|
3321
3438
|
throw new Error(
|
|
3322
|
-
`route '${this.#s.key}': Cannot combine .unprotected() and
|
|
3439
|
+
`route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
|
|
3323
3440
|
);
|
|
3324
3441
|
}
|
|
3325
3442
|
if (this.#s.pricing !== void 0) {
|
|
3326
3443
|
throw new Error(
|
|
3327
|
-
`route '${this.#s.key}': Cannot
|
|
3444
|
+
`route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
|
|
3328
3445
|
);
|
|
3329
3446
|
}
|
|
3330
3447
|
const next = this.fork();
|
|
3331
3448
|
next.#s.authMode = "paid";
|
|
3332
3449
|
next.#s.pricing = pricing;
|
|
3333
|
-
if (
|
|
3450
|
+
if (billing === "upto") {
|
|
3451
|
+
if (resolvedOptions.protocols?.some((p) => p !== "x402")) {
|
|
3452
|
+
throw new Error(
|
|
3453
|
+
`route '${this.#s.key}': .upTo() is x402-only \u2014 remove the conflicting protocols override.`
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
next.#s.protocols = ["x402"];
|
|
3457
|
+
} else if (billing === "metered") {
|
|
3458
|
+
if (resolvedOptions.protocols?.some((p) => p !== "mpp")) {
|
|
3459
|
+
throw new Error(
|
|
3460
|
+
`route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
3463
|
+
next.#s.protocols = ["mpp"];
|
|
3464
|
+
} else if (resolvedOptions.protocols) {
|
|
3334
3465
|
next.#s.protocols = [...resolvedOptions.protocols];
|
|
3335
3466
|
} else if (next.#s.protocols.length === 0) {
|
|
3336
3467
|
next.#s.protocols = ["x402"];
|
|
3337
3468
|
}
|
|
3338
|
-
if (resolvedOptions
|
|
3339
|
-
if (
|
|
3340
|
-
if (resolvedOptions
|
|
3341
|
-
if (resolvedOptions
|
|
3342
|
-
if (resolvedOptions
|
|
3343
|
-
|
|
3344
|
-
if (
|
|
3469
|
+
if (resolvedOptions.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
|
|
3470
|
+
if (maxPrice) next.#s.maxPrice = maxPrice;
|
|
3471
|
+
if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
|
|
3472
|
+
if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
|
|
3473
|
+
if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
|
|
3474
|
+
next.#s.billing = billing;
|
|
3475
|
+
if (tickCost) next.#s.tickCost = tickCost;
|
|
3476
|
+
if (unitType) next.#s.unitType = unitType;
|
|
3345
3477
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
3346
|
-
if (next.#s.dynamicPrice) {
|
|
3347
|
-
throw new Error(
|
|
3348
|
-
`route '${this.#s.key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
|
|
3349
|
-
);
|
|
3350
|
-
}
|
|
3351
3478
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
3352
3479
|
if (!tierKey) {
|
|
3353
3480
|
throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
|
|
@@ -3359,22 +3486,16 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3359
3486
|
}
|
|
3360
3487
|
}
|
|
3361
3488
|
}
|
|
3362
|
-
if (
|
|
3489
|
+
if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
|
|
3363
3490
|
throw new Error(
|
|
3364
|
-
`route '${this.#s.key}': maxPrice '${
|
|
3491
|
+
`route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
|
|
3365
3492
|
);
|
|
3366
3493
|
}
|
|
3367
|
-
if (
|
|
3494
|
+
if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
|
|
3368
3495
|
throw new Error(
|
|
3369
|
-
`route '${this.#s.key}': tickCost '${
|
|
3496
|
+
`route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
|
|
3370
3497
|
);
|
|
3371
3498
|
}
|
|
3372
|
-
if (next.#s.dynamicPrice && !next.#s.maxPrice) {
|
|
3373
|
-
throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires maxPrice`);
|
|
3374
|
-
}
|
|
3375
|
-
if (next.#s.dynamicPrice && !next.#s.tickCost) {
|
|
3376
|
-
throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires tickCost`);
|
|
3377
|
-
}
|
|
3378
3499
|
return next;
|
|
3379
3500
|
}
|
|
3380
3501
|
/**
|
|
@@ -3656,13 +3777,13 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3656
3777
|
/**
|
|
3657
3778
|
* Register a streaming handler (`async function*`) and return the Next.js
|
|
3658
3779
|
* route function. Each `charge()` call bills one tick (`tickCost` USDC) up
|
|
3659
|
-
* to `maxPrice`; requires `.
|
|
3780
|
+
* to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
|
|
3660
3781
|
*
|
|
3661
3782
|
* @example
|
|
3662
3783
|
* ```ts
|
|
3663
3784
|
* export const POST = router
|
|
3664
3785
|
* .route('llm/stream')
|
|
3665
|
-
* .
|
|
3786
|
+
* .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
|
|
3666
3787
|
* .body(schema)
|
|
3667
3788
|
* .stream(async function* ({ body, charge }) {
|
|
3668
3789
|
* for await (const token of streamLLM(body.prompt)) {
|
|
@@ -3678,7 +3799,7 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3678
3799
|
register(handlerFn, streaming) {
|
|
3679
3800
|
if (!this.#s.authMode) {
|
|
3680
3801
|
throw new Error(
|
|
3681
|
-
`route '${this.#s.key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
3802
|
+
`route '${this.#s.key}': Select an auth mode: .paid(pricing), .upTo(maxPrice), .metered(options), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
3682
3803
|
);
|
|
3683
3804
|
}
|
|
3684
3805
|
if (this.#s.validateFn && !this.#s.bodySchema) {
|
|
@@ -3689,24 +3810,34 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3689
3810
|
if (this.#s.settlement && !this.#s.pricing) {
|
|
3690
3811
|
throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
|
|
3691
3812
|
}
|
|
3692
|
-
if (this.#s.
|
|
3813
|
+
if (this.#s.billing === "upto") {
|
|
3693
3814
|
const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
|
|
3694
3815
|
if (!hasUpto) {
|
|
3695
3816
|
throw new Error(
|
|
3696
|
-
`route '${this.#s.key}': .
|
|
3817
|
+
`route '${this.#s.key}': .upTo() requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
if (this.#s.pricing !== void 0 && this.#s.billing === "exact" && this.#s.protocols.includes("x402")) {
|
|
3822
|
+
const hasExact = this.#s.deps.x402Accepts.some(
|
|
3823
|
+
(accept) => (accept.scheme ?? "exact") !== "upto"
|
|
3824
|
+
);
|
|
3825
|
+
if (!hasExact) {
|
|
3826
|
+
throw new Error(
|
|
3827
|
+
`route '${this.#s.key}': .paid() needs a non-'upto' x402 accept \u2014 an 'upto'-only accept list cannot serve a fixed-price route. Add { scheme: 'exact', network } to RouterConfig.x402.accepts, or use .upTo() for handler-computed billing.`
|
|
3697
3828
|
);
|
|
3698
3829
|
}
|
|
3699
3830
|
}
|
|
3700
|
-
if (this.#s.
|
|
3831
|
+
if (this.#s.billing === "metered") {
|
|
3701
3832
|
if (!this.#s.deps.mppSessionConfig) {
|
|
3702
3833
|
throw new Error(
|
|
3703
|
-
`route '${this.#s.key}': .
|
|
3834
|
+
`route '${this.#s.key}': .metered() requires MPP session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
|
|
3704
3835
|
);
|
|
3705
3836
|
}
|
|
3706
3837
|
}
|
|
3707
|
-
if (streaming &&
|
|
3838
|
+
if (streaming && this.#s.billing !== "metered") {
|
|
3708
3839
|
throw new Error(
|
|
3709
|
-
`route '${this.#s.key}': .stream() requires .
|
|
3840
|
+
`route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
|
|
3710
3841
|
);
|
|
3711
3842
|
}
|
|
3712
3843
|
validateExamples(
|
|
@@ -3724,7 +3855,7 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3724
3855
|
authMode: this.#s.authMode,
|
|
3725
3856
|
siwxEnabled: this.#s.siwxEnabled,
|
|
3726
3857
|
pricing: this.#s.pricing,
|
|
3727
|
-
|
|
3858
|
+
billing: this.#s.billing,
|
|
3728
3859
|
streaming: streaming ? true : void 0,
|
|
3729
3860
|
protocols: this.#s.protocols,
|
|
3730
3861
|
bodySchema: this.#s.bodySchema,
|
|
@@ -3751,16 +3882,59 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3751
3882
|
return createRequestHandler(entry, handlerFn, this.#s.deps);
|
|
3752
3883
|
}
|
|
3753
3884
|
};
|
|
3754
|
-
function
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3885
|
+
function normalizePaidArg(routeKey, arg, options) {
|
|
3886
|
+
if (typeof arg === "string") {
|
|
3887
|
+
return { pricing: arg, resolvedOptions: options ?? {}, billing: "exact" };
|
|
3888
|
+
}
|
|
3889
|
+
if (typeof arg === "function") {
|
|
3890
|
+
return {
|
|
3891
|
+
pricing: arg,
|
|
3892
|
+
resolvedOptions: options ?? {},
|
|
3893
|
+
billing: "exact"
|
|
3894
|
+
};
|
|
3762
3895
|
}
|
|
3763
|
-
|
|
3896
|
+
if ("tiers" in arg && "field" in arg) {
|
|
3897
|
+
return {
|
|
3898
|
+
pricing: { field: arg.field, tiers: arg.tiers, default: arg.default },
|
|
3899
|
+
resolvedOptions: arg,
|
|
3900
|
+
billing: "exact"
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
if ("price" in arg && typeof arg.price === "string") {
|
|
3904
|
+
return { pricing: arg.price, resolvedOptions: arg, billing: "exact" };
|
|
3905
|
+
}
|
|
3906
|
+
throw new Error(
|
|
3907
|
+
`route '${routeKey}': .paid() requires one of: a price string, a (body) => string function, { price }, or { field, tiers }. For handler-computed billing use .upTo(); for per-tick billing use .metered().`
|
|
3908
|
+
);
|
|
3909
|
+
}
|
|
3910
|
+
function normalizeUpToArg(routeKey, arg) {
|
|
3911
|
+
const options = typeof arg === "string" ? { maxPrice: arg } : arg;
|
|
3912
|
+
if (!options.maxPrice) {
|
|
3913
|
+
throw new Error(`route '${routeKey}': .upTo() requires maxPrice`);
|
|
3914
|
+
}
|
|
3915
|
+
return {
|
|
3916
|
+
pricing: options.maxPrice,
|
|
3917
|
+
resolvedOptions: options,
|
|
3918
|
+
billing: "upto",
|
|
3919
|
+
unitType: options.unitType,
|
|
3920
|
+
maxPrice: options.maxPrice
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
function normalizeMeteredArg(routeKey, options) {
|
|
3924
|
+
if (!options.maxPrice) {
|
|
3925
|
+
throw new Error(`route '${routeKey}': .metered() requires maxPrice`);
|
|
3926
|
+
}
|
|
3927
|
+
if (!options.tickCost) {
|
|
3928
|
+
throw new Error(`route '${routeKey}': .metered() requires tickCost`);
|
|
3929
|
+
}
|
|
3930
|
+
return {
|
|
3931
|
+
pricing: options.maxPrice,
|
|
3932
|
+
resolvedOptions: options,
|
|
3933
|
+
billing: "metered",
|
|
3934
|
+
tickCost: options.tickCost,
|
|
3935
|
+
unitType: options.unitType,
|
|
3936
|
+
maxPrice: options.maxPrice
|
|
3937
|
+
};
|
|
3764
3938
|
}
|
|
3765
3939
|
|
|
3766
3940
|
// src/discovery/well-known.ts
|
|
@@ -4699,7 +4873,7 @@ function createRouter(config) {
|
|
|
4699
4873
|
x402Accepts,
|
|
4700
4874
|
mppx: null,
|
|
4701
4875
|
tempoClient: null,
|
|
4702
|
-
mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
4876
|
+
mppSessionConfig: config.mpp?.session && config.mpp.operatorKey ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
4703
4877
|
};
|
|
4704
4878
|
deps.initPromise = (async () => {
|
|
4705
4879
|
const x402Result = await initX402(config, kvStore, x402ConfigError);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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": {
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"module": "./dist/index.js",
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"files": [
|
|
22
|
-
"dist",
|
|
22
|
+
"dist/index.js",
|
|
23
|
+
"dist/index.cjs",
|
|
24
|
+
"dist/index.d.ts",
|
|
25
|
+
"dist/index.d.cts",
|
|
23
26
|
"AGENTS.md",
|
|
24
27
|
"README.md"
|
|
25
28
|
],
|