@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/README.md
CHANGED
|
@@ -141,6 +141,8 @@ export const GET = router.openapi();
|
|
|
141
141
|
|
|
142
142
|
The barrel forces every route module to load before the discovery handler walks the registry — Next.js otherwise lazy-loads route files on first hit, and unloaded routes don't appear in the spec.
|
|
143
143
|
|
|
144
|
+
The `openapi.json` should be hosted at `GET <origin>/openapi.json`.
|
|
145
|
+
|
|
144
146
|
## Auth modes
|
|
145
147
|
|
|
146
148
|
| Method | Purpose |
|
package/dist/index.cjs
CHANGED
|
@@ -67,6 +67,9 @@ function getConfiguredX402Accepts(config) {
|
|
|
67
67
|
function getConfiguredX402Networks(config) {
|
|
68
68
|
return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
|
|
69
69
|
}
|
|
70
|
+
function selectRouteAccepts(accepts, routeEntry) {
|
|
71
|
+
return routeEntry.billing === "upto" ? accepts.filter((accept) => accept.scheme === "upto") : accepts.filter((accept) => (accept.scheme ?? "exact") !== "upto");
|
|
72
|
+
}
|
|
70
73
|
async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
|
|
71
74
|
return Promise.all(
|
|
72
75
|
accepts.map(async (accept) => ({
|
|
@@ -362,9 +365,18 @@ function withScopedKinds(client, kinds) {
|
|
|
362
365
|
return {
|
|
363
366
|
verify: client.verify.bind(client),
|
|
364
367
|
settle: client.settle.bind(client),
|
|
365
|
-
getSupported: async () =>
|
|
368
|
+
getSupported: async () => {
|
|
369
|
+
const live = await client.getSupported();
|
|
370
|
+
return { ...live, kinds: mergeKindExtras(kinds, live.kinds) };
|
|
371
|
+
}
|
|
366
372
|
};
|
|
367
373
|
}
|
|
374
|
+
function mergeKindExtras(scoped, live) {
|
|
375
|
+
return scoped.map((kind) => {
|
|
376
|
+
const match = live.find((l) => l.scheme === kind.scheme && l.network === kind.network);
|
|
377
|
+
return match?.extra ? { ...kind, extra: { ...kind.extra, ...match.extra } } : kind;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
368
380
|
function buildSupportedKinds(group) {
|
|
369
381
|
return group.networks.flatMap((network) => {
|
|
370
382
|
if (group.family === "solana") {
|
|
@@ -795,10 +807,7 @@ async function runHandler(ctx, handlerCtx) {
|
|
|
795
807
|
}
|
|
796
808
|
if (isAsyncIterable(returned) && !isThenable(returned)) {
|
|
797
809
|
return errorResult(
|
|
798
|
-
new HttpError(
|
|
799
|
-
`route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
|
|
800
|
-
500
|
|
801
|
-
)
|
|
810
|
+
new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
|
|
802
811
|
);
|
|
803
812
|
}
|
|
804
813
|
let rawResult;
|
|
@@ -1231,6 +1240,7 @@ var DynamicPricing = class {
|
|
|
1231
1240
|
const raw = await this.opts.fn(body);
|
|
1232
1241
|
return this.cap(raw, body);
|
|
1233
1242
|
} catch (err) {
|
|
1243
|
+
if (err instanceof HttpError) throw err;
|
|
1234
1244
|
this.alert("error", `Pricing function failed: ${msg(err)}`, {
|
|
1235
1245
|
error: err instanceof Error ? err.stack : String(err),
|
|
1236
1246
|
body
|
|
@@ -1753,7 +1763,7 @@ var mppStrategy = {
|
|
|
1753
1763
|
async verify(args) {
|
|
1754
1764
|
const info = readMppCredential(args.request);
|
|
1755
1765
|
if (!info) return { ok: false, kind: "invalid" };
|
|
1756
|
-
if (args.routeEntry.
|
|
1766
|
+
if (args.routeEntry.billing === "metered") {
|
|
1757
1767
|
if (!info.sessionAction) return { ok: false, kind: "invalid" };
|
|
1758
1768
|
return verifySessionMode(args, info);
|
|
1759
1769
|
}
|
|
@@ -1804,7 +1814,7 @@ var mppStrategy = {
|
|
|
1804
1814
|
async buildChallenge(args) {
|
|
1805
1815
|
if (!args.deps.mppx) return {};
|
|
1806
1816
|
const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
|
|
1807
|
-
if (args.routeEntry.
|
|
1817
|
+
if (args.routeEntry.billing === "metered" && sessionsConfigured) {
|
|
1808
1818
|
const tickCost = args.routeEntry.tickCost;
|
|
1809
1819
|
const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
|
|
1810
1820
|
const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
|
|
@@ -2061,7 +2071,7 @@ function tagBareDecimalAsDollars(amount) {
|
|
|
2061
2071
|
}
|
|
2062
2072
|
|
|
2063
2073
|
// src/protocols/x402/verify.ts
|
|
2064
|
-
var
|
|
2074
|
+
var import_types3 = require("@x402/core/types");
|
|
2065
2075
|
async function verifyX402Payment(opts) {
|
|
2066
2076
|
const { server, request, price, accepts, report } = opts;
|
|
2067
2077
|
const payload = await readPaymentPayload(request);
|
|
@@ -2080,7 +2090,7 @@ async function verifyX402Payment(opts) {
|
|
|
2080
2090
|
try {
|
|
2081
2091
|
verify = await server.verifyPayment(payload, matching);
|
|
2082
2092
|
} catch (err) {
|
|
2083
|
-
if (err instanceof
|
|
2093
|
+
if (err instanceof import_types3.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
|
|
2084
2094
|
return invalidPaymentVerification({
|
|
2085
2095
|
reason: err.invalidReason ?? "verify_error",
|
|
2086
2096
|
...err.invalidMessage ? { message: err.invalidMessage } : {},
|
|
@@ -2181,7 +2191,7 @@ async function verifyX402(args) {
|
|
|
2181
2191
|
const accepts = await resolveX402Accepts(
|
|
2182
2192
|
request,
|
|
2183
2193
|
routeEntry,
|
|
2184
|
-
deps.x402Accepts,
|
|
2194
|
+
selectRouteAccepts(deps.x402Accepts, routeEntry),
|
|
2185
2195
|
deps.payeeAddress,
|
|
2186
2196
|
body
|
|
2187
2197
|
);
|
|
@@ -2229,7 +2239,7 @@ async function verifyX402(args) {
|
|
|
2229
2239
|
async function settleX402(args) {
|
|
2230
2240
|
const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
|
|
2231
2241
|
const { payload, requirements } = token;
|
|
2232
|
-
const override = routeEntry.
|
|
2242
|
+
const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
|
|
2233
2243
|
try {
|
|
2234
2244
|
const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
|
|
2235
2245
|
if (!settle.result?.success) {
|
|
@@ -2259,7 +2269,7 @@ async function buildX402ChallengeContribution(args) {
|
|
|
2259
2269
|
const accepts = await resolveX402Accepts(
|
|
2260
2270
|
request,
|
|
2261
2271
|
routeEntry,
|
|
2262
|
-
deps.x402Accepts,
|
|
2272
|
+
selectRouteAccepts(deps.x402Accepts, routeEntry),
|
|
2263
2273
|
deps.payeeAddress,
|
|
2264
2274
|
body
|
|
2265
2275
|
);
|
|
@@ -2352,9 +2362,7 @@ async function buildChallengeExtensions(ctx) {
|
|
|
2352
2362
|
} catch {
|
|
2353
2363
|
}
|
|
2354
2364
|
}
|
|
2355
|
-
const hasEvmUpto = ctx.deps.x402Accepts.some(
|
|
2356
|
-
(accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
|
|
2357
|
-
);
|
|
2365
|
+
const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
|
|
2358
2366
|
if (hasEvmUpto) {
|
|
2359
2367
|
try {
|
|
2360
2368
|
const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
|
|
@@ -2521,10 +2529,7 @@ async function runDynamicChannelMgmtFlow(args) {
|
|
|
2521
2529
|
});
|
|
2522
2530
|
}
|
|
2523
2531
|
|
|
2524
|
-
// src/
|
|
2525
|
-
var import_server6 = require("next/server");
|
|
2526
|
-
|
|
2527
|
-
// src/pricing/charge-context.ts
|
|
2532
|
+
// src/pricing/metered-charge.ts
|
|
2528
2533
|
function createChargeContext(args) {
|
|
2529
2534
|
const { tickCost, maxPrice, route } = args;
|
|
2530
2535
|
const tickAtomic = decimalToAtomic(tickCost);
|
|
@@ -2559,15 +2564,10 @@ function createChargeContext(args) {
|
|
|
2559
2564
|
};
|
|
2560
2565
|
}
|
|
2561
2566
|
|
|
2562
|
-
// src/pipeline/flows/dynamic/dynamic-invoke.ts
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
tickCost: ctx.routeEntry.tickCost,
|
|
2567
|
-
maxPrice: ctx.routeEntry.maxPrice,
|
|
2568
|
-
route: ctx.routeEntry.key
|
|
2569
|
-
}) : null;
|
|
2570
|
-
const baseHandlerCtx = {
|
|
2567
|
+
// src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
|
|
2568
|
+
var import_server6 = require("next/server");
|
|
2569
|
+
function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
|
|
2570
|
+
return {
|
|
2571
2571
|
body,
|
|
2572
2572
|
query: parseQuery(ctx.request, ctx.routeEntry),
|
|
2573
2573
|
request: ctx.request,
|
|
@@ -2579,12 +2579,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
|
2579
2579
|
alert: ctx.report,
|
|
2580
2580
|
setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
|
|
2581
2581
|
};
|
|
2582
|
+
}
|
|
2583
|
+
function toResponse(rawResult) {
|
|
2584
|
+
return rawResult instanceof Response ? rawResult : import_server6.NextResponse.json(rawResult);
|
|
2585
|
+
}
|
|
2586
|
+
function errorResult2(error) {
|
|
2587
|
+
const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
|
|
2588
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
2589
|
+
return {
|
|
2590
|
+
kind: "request",
|
|
2591
|
+
response: import_server6.NextResponse.json({ success: false, error: message }, { status }),
|
|
2592
|
+
rawResult: void 0,
|
|
2593
|
+
handlerError: error
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
function isAsyncIterable2(value) {
|
|
2597
|
+
return value != null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
2598
|
+
}
|
|
2599
|
+
function isThenable2(value) {
|
|
2600
|
+
return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
|
|
2604
|
+
async function invokeMetered(ctx, wallet, account, body, payment) {
|
|
2605
|
+
const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
|
|
2606
|
+
tickCost: ctx.routeEntry.tickCost,
|
|
2607
|
+
maxPrice: ctx.routeEntry.maxPrice,
|
|
2608
|
+
route: ctx.routeEntry.key
|
|
2609
|
+
}) : null;
|
|
2610
|
+
const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
|
|
2582
2611
|
const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
|
|
2583
2612
|
let returned;
|
|
2584
2613
|
try {
|
|
2585
2614
|
returned = ctx.handler(handlerCtx);
|
|
2586
2615
|
} catch (error) {
|
|
2587
|
-
return errorResult2(error
|
|
2616
|
+
return errorResult2(error);
|
|
2588
2617
|
}
|
|
2589
2618
|
if (isAsyncIterable2(returned) && !isThenable2(returned)) {
|
|
2590
2619
|
if (!chargeContext) {
|
|
@@ -2592,41 +2621,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
|
|
|
2592
2621
|
new HttpError(
|
|
2593
2622
|
"route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
|
|
2594
2623
|
500
|
|
2595
|
-
)
|
|
2596
|
-
null
|
|
2624
|
+
)
|
|
2597
2625
|
);
|
|
2598
2626
|
}
|
|
2599
|
-
return {
|
|
2600
|
-
kind: "stream",
|
|
2601
|
-
source: returned,
|
|
2602
|
-
chargeContext
|
|
2603
|
-
};
|
|
2627
|
+
return { kind: "stream", source: returned, chargeContext };
|
|
2604
2628
|
}
|
|
2605
2629
|
let rawResult;
|
|
2606
2630
|
try {
|
|
2607
2631
|
rawResult = await returned;
|
|
2608
2632
|
} catch (error) {
|
|
2609
|
-
return errorResult2(error
|
|
2633
|
+
return errorResult2(error);
|
|
2610
2634
|
}
|
|
2611
|
-
|
|
2612
|
-
return { kind: "request", response, rawResult };
|
|
2635
|
+
return { kind: "request", response: toResponse(rawResult), rawResult };
|
|
2613
2636
|
}
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2637
|
+
|
|
2638
|
+
// src/pricing/upto-charge.ts
|
|
2639
|
+
function createUptoChargeContext(args) {
|
|
2640
|
+
const { maxPrice, route } = args;
|
|
2641
|
+
const capAtomic = decimalToAtomic(maxPrice);
|
|
2642
|
+
if (capAtomic <= 0n) {
|
|
2643
|
+
throw new Error(`route '${route}': maxPrice '${maxPrice}' must be a positive decimal string`);
|
|
2644
|
+
}
|
|
2645
|
+
let calls = 0;
|
|
2646
|
+
let atomic = 0n;
|
|
2647
|
+
const charge = async (amount) => {
|
|
2648
|
+
const nextAtomic = atomic + decimalToAtomic(amount);
|
|
2649
|
+
if (nextAtomic > capAtomic) {
|
|
2650
|
+
throw Object.assign(
|
|
2651
|
+
new Error(
|
|
2652
|
+
`route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
|
|
2653
|
+
),
|
|
2654
|
+
{ status: 400, code: "CHARGE_OVER_CAP" }
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
calls += 1;
|
|
2658
|
+
atomic = nextAtomic;
|
|
2659
|
+
};
|
|
2618
2660
|
return {
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
handlerError: error
|
|
2661
|
+
charge,
|
|
2662
|
+
callCount: () => calls,
|
|
2663
|
+
atomicTotal: () => atomic
|
|
2623
2664
|
};
|
|
2624
2665
|
}
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2666
|
+
|
|
2667
|
+
// src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
|
|
2668
|
+
async function invokeUpto(ctx, wallet, account, body, payment) {
|
|
2669
|
+
const uptoCtx = createUptoChargeContext({
|
|
2670
|
+
maxPrice: ctx.routeEntry.maxPrice,
|
|
2671
|
+
route: ctx.routeEntry.key
|
|
2672
|
+
});
|
|
2673
|
+
const handlerCtx = {
|
|
2674
|
+
...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
|
|
2675
|
+
charge: uptoCtx.charge
|
|
2676
|
+
};
|
|
2677
|
+
let returned;
|
|
2678
|
+
try {
|
|
2679
|
+
returned = ctx.handler(handlerCtx);
|
|
2680
|
+
} catch (error) {
|
|
2681
|
+
return errorResult2(error);
|
|
2682
|
+
}
|
|
2683
|
+
if (isAsyncIterable2(returned) && !isThenable2(returned)) {
|
|
2684
|
+
return errorResult2(
|
|
2685
|
+
new HttpError(
|
|
2686
|
+
"streaming is not supported on .upTo() routes \u2014 return a value from .handler() instead",
|
|
2687
|
+
500
|
|
2688
|
+
)
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
let rawResult;
|
|
2692
|
+
try {
|
|
2693
|
+
rawResult = await returned;
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
return errorResult2(error);
|
|
2696
|
+
}
|
|
2697
|
+
return { kind: "request", response: toResponse(rawResult), rawResult, uptoContext: uptoCtx };
|
|
2630
2698
|
}
|
|
2631
2699
|
|
|
2632
2700
|
// src/pipeline/flows/dynamic/dynamic-preflight.ts
|
|
@@ -2656,7 +2724,7 @@ async function runDynamicRequestFlow(args) {
|
|
|
2656
2724
|
}
|
|
2657
2725
|
const beforeErr = await runBeforeSettle(ctx, settleScope);
|
|
2658
2726
|
if (beforeErr) return beforeErr;
|
|
2659
|
-
const billedAmount = routeEntry
|
|
2727
|
+
const billedAmount = computeBilledAmount(routeEntry, result);
|
|
2660
2728
|
return settleAndFinalizeRequest({
|
|
2661
2729
|
ctx,
|
|
2662
2730
|
strategy,
|
|
@@ -2674,6 +2742,19 @@ async function runDynamicRequestFlow(args) {
|
|
|
2674
2742
|
}
|
|
2675
2743
|
});
|
|
2676
2744
|
}
|
|
2745
|
+
function computeBilledAmount(routeEntry, result) {
|
|
2746
|
+
if (routeEntry.billing === "upto") {
|
|
2747
|
+
const total = result.uptoContext?.atomicTotal() ?? 0n;
|
|
2748
|
+
if (total <= 0n) {
|
|
2749
|
+
throw new HttpError(
|
|
2750
|
+
`route '${routeEntry.key}': handler did not call charge(amount) \u2014 upto routes must accumulate a non-zero billed amount`,
|
|
2751
|
+
500
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
return atomicToDecimal(total);
|
|
2755
|
+
}
|
|
2756
|
+
return routeEntry.tickCost;
|
|
2757
|
+
}
|
|
2677
2758
|
|
|
2678
2759
|
// src/pipeline/flows/dynamic/dynamic-stream.ts
|
|
2679
2760
|
async function runDynamicStreamFlow(args) {
|
|
@@ -2746,13 +2827,7 @@ async function runDynamicPaidFlow(ctx) {
|
|
|
2746
2827
|
amount: price,
|
|
2747
2828
|
network: verifyOutcome.payment.network
|
|
2748
2829
|
});
|
|
2749
|
-
const result = await invokeDynamic(
|
|
2750
|
-
ctx,
|
|
2751
|
-
verifyOutcome.wallet,
|
|
2752
|
-
account,
|
|
2753
|
-
parsedBody,
|
|
2754
|
-
verifyOutcome.payment
|
|
2755
|
-
);
|
|
2830
|
+
const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
|
|
2756
2831
|
switch (result.kind) {
|
|
2757
2832
|
case "stream":
|
|
2758
2833
|
return runDynamicStreamFlow({
|
|
@@ -2774,6 +2849,18 @@ async function runDynamicPaidFlow(ctx) {
|
|
|
2774
2849
|
});
|
|
2775
2850
|
}
|
|
2776
2851
|
}
|
|
2852
|
+
async function invokeDynamic(ctx, verifyOutcome, account, parsedBody) {
|
|
2853
|
+
switch (ctx.routeEntry.billing) {
|
|
2854
|
+
case "upto":
|
|
2855
|
+
return invokeUpto(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
|
|
2856
|
+
case "metered":
|
|
2857
|
+
return invokeMetered(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
|
|
2858
|
+
case "exact":
|
|
2859
|
+
throw new Error(
|
|
2860
|
+
`route '${ctx.routeEntry.key}': exact billing must not reach the dynamic paid flow`
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2777
2864
|
|
|
2778
2865
|
// src/pipeline/flows/static/static-body-and-price.ts
|
|
2779
2866
|
async function resolveStaticBodyAndPrice(args) {
|
|
@@ -2921,13 +3008,8 @@ async function runStaticPaidFlow(ctx) {
|
|
|
2921
3008
|
|
|
2922
3009
|
// src/pipeline/flows/paid.ts
|
|
2923
3010
|
async function runPaidFlow(ctx) {
|
|
2924
|
-
const
|
|
2925
|
-
|
|
2926
|
-
case true:
|
|
2927
|
-
return runDynamicPaidFlow(ctx);
|
|
2928
|
-
case false:
|
|
2929
|
-
return runStaticPaidFlow(ctx);
|
|
2930
|
-
}
|
|
3011
|
+
const handlerCharged = ctx.routeEntry.billing !== "exact";
|
|
3012
|
+
return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
|
|
2931
3013
|
}
|
|
2932
3014
|
|
|
2933
3015
|
// src/pipeline/flows/siwx-only.ts
|
|
@@ -3325,7 +3407,7 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3325
3407
|
protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
|
|
3326
3408
|
maxPrice: void 0,
|
|
3327
3409
|
minPrice: void 0,
|
|
3328
|
-
|
|
3410
|
+
billing: "exact",
|
|
3329
3411
|
tickCost: void 0,
|
|
3330
3412
|
unitType: void 0,
|
|
3331
3413
|
payTo: void 0,
|
|
@@ -3356,39 +3438,84 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3356
3438
|
next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
|
|
3357
3439
|
return next;
|
|
3358
3440
|
}
|
|
3359
|
-
paid(
|
|
3360
|
-
|
|
3441
|
+
paid(arg, options) {
|
|
3442
|
+
return this.applyPaid(normalizePaidArg(this.#s.key, arg, options), "paid");
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* x402-only handler-computed billing. The handler receives `charge(amount)`
|
|
3446
|
+
* and the request settles once for the accumulated total, capped at
|
|
3447
|
+
* `maxPrice`. Requires an `'upto'` accept on at least one configured network.
|
|
3448
|
+
* Pass a bare string as sugar for `{ maxPrice }`.
|
|
3449
|
+
*
|
|
3450
|
+
* @example
|
|
3451
|
+
* ```ts
|
|
3452
|
+
* router.route('llm')
|
|
3453
|
+
* .upTo('0.05')
|
|
3454
|
+
* .body(schema)
|
|
3455
|
+
* .handler(async ({ body, charge }) => { await charge('0.001'); ... });
|
|
3456
|
+
* ```
|
|
3457
|
+
*/
|
|
3458
|
+
upTo(arg) {
|
|
3459
|
+
return this.applyPaid(normalizeUpToArg(this.#s.key, arg), "upTo");
|
|
3460
|
+
}
|
|
3461
|
+
/**
|
|
3462
|
+
* MPP-only per-tick billing. `.handler()` bills exactly `tickCost`;
|
|
3463
|
+
* `.stream()` calls `charge()` (no-arg) per yield, settling per tick up to
|
|
3464
|
+
* `maxPrice`. Requires `RouterConfig.mpp.session`.
|
|
3465
|
+
*
|
|
3466
|
+
* @example
|
|
3467
|
+
* ```ts
|
|
3468
|
+
* router.route('llm/stream')
|
|
3469
|
+
* .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
|
|
3470
|
+
* .stream(async function* ({ charge }) { await charge(); yield 'hi'; });
|
|
3471
|
+
* ```
|
|
3472
|
+
*/
|
|
3473
|
+
metered(options) {
|
|
3474
|
+
return this.applyPaid(normalizeMeteredArg(this.#s.key, options), "metered");
|
|
3475
|
+
}
|
|
3476
|
+
applyPaid(normalized, method) {
|
|
3477
|
+
const { pricing, resolvedOptions, billing, tickCost, unitType, maxPrice } = normalized;
|
|
3361
3478
|
if (this.#s.authMode === "unprotected") {
|
|
3362
3479
|
throw new Error(
|
|
3363
|
-
`route '${this.#s.key}': Cannot combine .unprotected() and
|
|
3480
|
+
`route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
|
|
3364
3481
|
);
|
|
3365
3482
|
}
|
|
3366
3483
|
if (this.#s.pricing !== void 0) {
|
|
3367
3484
|
throw new Error(
|
|
3368
|
-
`route '${this.#s.key}': Cannot
|
|
3485
|
+
`route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
|
|
3369
3486
|
);
|
|
3370
3487
|
}
|
|
3371
3488
|
const next = this.fork();
|
|
3372
3489
|
next.#s.authMode = "paid";
|
|
3373
3490
|
next.#s.pricing = pricing;
|
|
3374
|
-
if (
|
|
3491
|
+
if (billing === "upto") {
|
|
3492
|
+
if (resolvedOptions.protocols?.some((p) => p !== "x402")) {
|
|
3493
|
+
throw new Error(
|
|
3494
|
+
`route '${this.#s.key}': .upTo() is x402-only \u2014 remove the conflicting protocols override.`
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
next.#s.protocols = ["x402"];
|
|
3498
|
+
} else if (billing === "metered") {
|
|
3499
|
+
if (resolvedOptions.protocols?.some((p) => p !== "mpp")) {
|
|
3500
|
+
throw new Error(
|
|
3501
|
+
`route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
next.#s.protocols = ["mpp"];
|
|
3505
|
+
} else if (resolvedOptions.protocols) {
|
|
3375
3506
|
next.#s.protocols = [...resolvedOptions.protocols];
|
|
3376
3507
|
} else if (next.#s.protocols.length === 0) {
|
|
3377
3508
|
next.#s.protocols = ["x402"];
|
|
3378
3509
|
}
|
|
3379
|
-
if (resolvedOptions
|
|
3380
|
-
if (
|
|
3381
|
-
if (resolvedOptions
|
|
3382
|
-
if (resolvedOptions
|
|
3383
|
-
if (resolvedOptions
|
|
3384
|
-
|
|
3385
|
-
if (
|
|
3510
|
+
if (resolvedOptions.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
|
|
3511
|
+
if (maxPrice) next.#s.maxPrice = maxPrice;
|
|
3512
|
+
if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
|
|
3513
|
+
if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
|
|
3514
|
+
if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
|
|
3515
|
+
next.#s.billing = billing;
|
|
3516
|
+
if (tickCost) next.#s.tickCost = tickCost;
|
|
3517
|
+
if (unitType) next.#s.unitType = unitType;
|
|
3386
3518
|
if (typeof pricing === "object" && "tiers" in pricing) {
|
|
3387
|
-
if (next.#s.dynamicPrice) {
|
|
3388
|
-
throw new Error(
|
|
3389
|
-
`route '${this.#s.key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
|
|
3390
|
-
);
|
|
3391
|
-
}
|
|
3392
3519
|
for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
|
|
3393
3520
|
if (!tierKey) {
|
|
3394
3521
|
throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
|
|
@@ -3400,22 +3527,16 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3400
3527
|
}
|
|
3401
3528
|
}
|
|
3402
3529
|
}
|
|
3403
|
-
if (
|
|
3530
|
+
if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
|
|
3404
3531
|
throw new Error(
|
|
3405
|
-
`route '${this.#s.key}': maxPrice '${
|
|
3532
|
+
`route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
|
|
3406
3533
|
);
|
|
3407
3534
|
}
|
|
3408
|
-
if (
|
|
3535
|
+
if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
|
|
3409
3536
|
throw new Error(
|
|
3410
|
-
`route '${this.#s.key}': tickCost '${
|
|
3537
|
+
`route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
|
|
3411
3538
|
);
|
|
3412
3539
|
}
|
|
3413
|
-
if (next.#s.dynamicPrice && !next.#s.maxPrice) {
|
|
3414
|
-
throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires maxPrice`);
|
|
3415
|
-
}
|
|
3416
|
-
if (next.#s.dynamicPrice && !next.#s.tickCost) {
|
|
3417
|
-
throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires tickCost`);
|
|
3418
|
-
}
|
|
3419
3540
|
return next;
|
|
3420
3541
|
}
|
|
3421
3542
|
/**
|
|
@@ -3697,13 +3818,13 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3697
3818
|
/**
|
|
3698
3819
|
* Register a streaming handler (`async function*`) and return the Next.js
|
|
3699
3820
|
* route function. Each `charge()` call bills one tick (`tickCost` USDC) up
|
|
3700
|
-
* to `maxPrice`; requires `.
|
|
3821
|
+
* to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
|
|
3701
3822
|
*
|
|
3702
3823
|
* @example
|
|
3703
3824
|
* ```ts
|
|
3704
3825
|
* export const POST = router
|
|
3705
3826
|
* .route('llm/stream')
|
|
3706
|
-
* .
|
|
3827
|
+
* .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
|
|
3707
3828
|
* .body(schema)
|
|
3708
3829
|
* .stream(async function* ({ body, charge }) {
|
|
3709
3830
|
* for await (const token of streamLLM(body.prompt)) {
|
|
@@ -3719,7 +3840,7 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3719
3840
|
register(handlerFn, streaming) {
|
|
3720
3841
|
if (!this.#s.authMode) {
|
|
3721
3842
|
throw new Error(
|
|
3722
|
-
`route '${this.#s.key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
3843
|
+
`route '${this.#s.key}': Select an auth mode: .paid(pricing), .upTo(maxPrice), .metered(options), .siwx(), .apiKey(resolver), or .unprotected()`
|
|
3723
3844
|
);
|
|
3724
3845
|
}
|
|
3725
3846
|
if (this.#s.validateFn && !this.#s.bodySchema) {
|
|
@@ -3730,24 +3851,34 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3730
3851
|
if (this.#s.settlement && !this.#s.pricing) {
|
|
3731
3852
|
throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
|
|
3732
3853
|
}
|
|
3733
|
-
if (this.#s.
|
|
3854
|
+
if (this.#s.billing === "upto") {
|
|
3734
3855
|
const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
|
|
3735
3856
|
if (!hasUpto) {
|
|
3736
3857
|
throw new Error(
|
|
3737
|
-
`route '${this.#s.key}': .
|
|
3858
|
+
`route '${this.#s.key}': .upTo() requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
|
|
3859
|
+
);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
if (this.#s.pricing !== void 0 && this.#s.billing === "exact" && this.#s.protocols.includes("x402")) {
|
|
3863
|
+
const hasExact = this.#s.deps.x402Accepts.some(
|
|
3864
|
+
(accept) => (accept.scheme ?? "exact") !== "upto"
|
|
3865
|
+
);
|
|
3866
|
+
if (!hasExact) {
|
|
3867
|
+
throw new Error(
|
|
3868
|
+
`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.`
|
|
3738
3869
|
);
|
|
3739
3870
|
}
|
|
3740
3871
|
}
|
|
3741
|
-
if (this.#s.
|
|
3872
|
+
if (this.#s.billing === "metered") {
|
|
3742
3873
|
if (!this.#s.deps.mppSessionConfig) {
|
|
3743
3874
|
throw new Error(
|
|
3744
|
-
`route '${this.#s.key}': .
|
|
3875
|
+
`route '${this.#s.key}': .metered() requires MPP session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
|
|
3745
3876
|
);
|
|
3746
3877
|
}
|
|
3747
3878
|
}
|
|
3748
|
-
if (streaming &&
|
|
3879
|
+
if (streaming && this.#s.billing !== "metered") {
|
|
3749
3880
|
throw new Error(
|
|
3750
|
-
`route '${this.#s.key}': .stream() requires .
|
|
3881
|
+
`route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
|
|
3751
3882
|
);
|
|
3752
3883
|
}
|
|
3753
3884
|
validateExamples(
|
|
@@ -3765,7 +3896,7 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3765
3896
|
authMode: this.#s.authMode,
|
|
3766
3897
|
siwxEnabled: this.#s.siwxEnabled,
|
|
3767
3898
|
pricing: this.#s.pricing,
|
|
3768
|
-
|
|
3899
|
+
billing: this.#s.billing,
|
|
3769
3900
|
streaming: streaming ? true : void 0,
|
|
3770
3901
|
protocols: this.#s.protocols,
|
|
3771
3902
|
bodySchema: this.#s.bodySchema,
|
|
@@ -3792,16 +3923,59 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
3792
3923
|
return createRequestHandler(entry, handlerFn, this.#s.deps);
|
|
3793
3924
|
}
|
|
3794
3925
|
};
|
|
3795
|
-
function
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3926
|
+
function normalizePaidArg(routeKey, arg, options) {
|
|
3927
|
+
if (typeof arg === "string") {
|
|
3928
|
+
return { pricing: arg, resolvedOptions: options ?? {}, billing: "exact" };
|
|
3929
|
+
}
|
|
3930
|
+
if (typeof arg === "function") {
|
|
3931
|
+
return {
|
|
3932
|
+
pricing: arg,
|
|
3933
|
+
resolvedOptions: options ?? {},
|
|
3934
|
+
billing: "exact"
|
|
3935
|
+
};
|
|
3803
3936
|
}
|
|
3804
|
-
|
|
3937
|
+
if ("tiers" in arg && "field" in arg) {
|
|
3938
|
+
return {
|
|
3939
|
+
pricing: { field: arg.field, tiers: arg.tiers, default: arg.default },
|
|
3940
|
+
resolvedOptions: arg,
|
|
3941
|
+
billing: "exact"
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
if ("price" in arg && typeof arg.price === "string") {
|
|
3945
|
+
return { pricing: arg.price, resolvedOptions: arg, billing: "exact" };
|
|
3946
|
+
}
|
|
3947
|
+
throw new Error(
|
|
3948
|
+
`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().`
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
3951
|
+
function normalizeUpToArg(routeKey, arg) {
|
|
3952
|
+
const options = typeof arg === "string" ? { maxPrice: arg } : arg;
|
|
3953
|
+
if (!options.maxPrice) {
|
|
3954
|
+
throw new Error(`route '${routeKey}': .upTo() requires maxPrice`);
|
|
3955
|
+
}
|
|
3956
|
+
return {
|
|
3957
|
+
pricing: options.maxPrice,
|
|
3958
|
+
resolvedOptions: options,
|
|
3959
|
+
billing: "upto",
|
|
3960
|
+
unitType: options.unitType,
|
|
3961
|
+
maxPrice: options.maxPrice
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
function normalizeMeteredArg(routeKey, options) {
|
|
3965
|
+
if (!options.maxPrice) {
|
|
3966
|
+
throw new Error(`route '${routeKey}': .metered() requires maxPrice`);
|
|
3967
|
+
}
|
|
3968
|
+
if (!options.tickCost) {
|
|
3969
|
+
throw new Error(`route '${routeKey}': .metered() requires tickCost`);
|
|
3970
|
+
}
|
|
3971
|
+
return {
|
|
3972
|
+
pricing: options.maxPrice,
|
|
3973
|
+
resolvedOptions: options,
|
|
3974
|
+
billing: "metered",
|
|
3975
|
+
tickCost: options.tickCost,
|
|
3976
|
+
unitType: options.unitType,
|
|
3977
|
+
maxPrice: options.maxPrice
|
|
3978
|
+
};
|
|
3805
3979
|
}
|
|
3806
3980
|
|
|
3807
3981
|
// src/discovery/well-known.ts
|
|
@@ -4740,7 +4914,7 @@ function createRouter(config) {
|
|
|
4740
4914
|
x402Accepts,
|
|
4741
4915
|
mppx: null,
|
|
4742
4916
|
tempoClient: null,
|
|
4743
|
-
mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
4917
|
+
mppSessionConfig: config.mpp?.session && config.mpp.operatorKey ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
|
|
4744
4918
|
};
|
|
4745
4919
|
deps.initPromise = (async () => {
|
|
4746
4920
|
const x402Result = await initX402(config, kvStore, x402ConfigError);
|