@vinaystwt/xmpp-router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/router/src/index.d.ts +23 -0
- package/dist/router/src/index.js +432 -0
- package/dist/router/src/index.test.d.ts +1 -0
- package/dist/router/src/index.test.js +76 -0
- package/dist/types/src/index.d.ts +343 -0
- package/dist/types/src/index.js +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xMPP contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PaymentChallenge, RouteContext, RouteDecision, RouteKind, ServiceCatalogEntry, WorkflowEstimateResult, WorkflowEstimateStep } from '@xmpp/types';
|
|
2
|
+
export declare function resolveCatalogEntry(input: RouteContext): ServiceCatalogEntry;
|
|
3
|
+
export declare function estimateRouteCost(input: {
|
|
4
|
+
route: RouteKind;
|
|
5
|
+
url: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
serviceId?: string;
|
|
8
|
+
projectedRequests?: number;
|
|
9
|
+
hasReusableSession?: boolean;
|
|
10
|
+
}): number;
|
|
11
|
+
export declare function getServiceCatalog(): ServiceCatalogEntry[];
|
|
12
|
+
export declare function getServiceCatalogEntry(serviceId: string): ServiceCatalogEntry | null;
|
|
13
|
+
export declare function createRouter(): {
|
|
14
|
+
preview(input: RouteContext): Promise<RouteDecision>;
|
|
15
|
+
chooseFromChallenge(input: RouteContext & {
|
|
16
|
+
challenge: PaymentChallenge;
|
|
17
|
+
hasReusableSession?: boolean;
|
|
18
|
+
}): Promise<RouteDecision>;
|
|
19
|
+
explain(input: RouteContext & {
|
|
20
|
+
hasReusableSession?: boolean;
|
|
21
|
+
}): RouteDecision;
|
|
22
|
+
estimateWorkflow(steps: WorkflowEstimateStep[]): WorkflowEstimateResult;
|
|
23
|
+
};
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
const serviceCatalog = [
|
|
2
|
+
{
|
|
3
|
+
serviceId: 'research-api',
|
|
4
|
+
displayName: 'Research API',
|
|
5
|
+
description: 'Low-cost research lookups optimized for exact one-off payments.',
|
|
6
|
+
baseUrl: 'http://localhost:4101',
|
|
7
|
+
capabilities: {
|
|
8
|
+
x402: true,
|
|
9
|
+
mppCharge: false,
|
|
10
|
+
mppSession: false,
|
|
11
|
+
},
|
|
12
|
+
pricing: {
|
|
13
|
+
x402PerCallUsd: 0.01,
|
|
14
|
+
mppChargePerCallUsd: 0.03,
|
|
15
|
+
mppSessionOpenUsd: 0.005,
|
|
16
|
+
mppSessionPerCallUsd: 0.005,
|
|
17
|
+
},
|
|
18
|
+
routingHints: {
|
|
19
|
+
breakEvenCalls: 4,
|
|
20
|
+
streamingPreferred: false,
|
|
21
|
+
preferredSingleCall: 'x402',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
serviceId: 'market-api',
|
|
26
|
+
displayName: 'Market API',
|
|
27
|
+
description: 'Premium market data endpoint exposed through MPP HTTP payment auth.',
|
|
28
|
+
baseUrl: 'http://localhost:4102',
|
|
29
|
+
capabilities: {
|
|
30
|
+
x402: false,
|
|
31
|
+
mppCharge: true,
|
|
32
|
+
mppSession: false,
|
|
33
|
+
},
|
|
34
|
+
pricing: {
|
|
35
|
+
x402PerCallUsd: 0.04,
|
|
36
|
+
mppChargePerCallUsd: 0.03,
|
|
37
|
+
mppSessionOpenUsd: 0.01,
|
|
38
|
+
mppSessionPerCallUsd: 0.01,
|
|
39
|
+
},
|
|
40
|
+
routingHints: {
|
|
41
|
+
breakEvenCalls: 3,
|
|
42
|
+
streamingPreferred: false,
|
|
43
|
+
preferredSingleCall: 'mpp-charge',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
serviceId: 'stream-api',
|
|
48
|
+
displayName: 'Stream API',
|
|
49
|
+
description: 'Repeated tick stream designed to amortize cost over an MPP session.',
|
|
50
|
+
baseUrl: 'http://localhost:4103',
|
|
51
|
+
capabilities: {
|
|
52
|
+
x402: false,
|
|
53
|
+
mppCharge: false,
|
|
54
|
+
mppSession: true,
|
|
55
|
+
},
|
|
56
|
+
pricing: {
|
|
57
|
+
x402PerCallUsd: 0.01,
|
|
58
|
+
mppChargePerCallUsd: 0.03,
|
|
59
|
+
mppSessionOpenUsd: 0.005,
|
|
60
|
+
mppSessionPerCallUsd: 0.005,
|
|
61
|
+
},
|
|
62
|
+
routingHints: {
|
|
63
|
+
breakEvenCalls: 4,
|
|
64
|
+
streamingPreferred: true,
|
|
65
|
+
preferredSingleCall: 'x402',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
const catalogByServiceId = new Map(serviceCatalog.map((entry) => [entry.serviceId, entry]));
|
|
70
|
+
const discoveredCatalog = new Map();
|
|
71
|
+
const discoveryTimestamps = new Map();
|
|
72
|
+
const discoveryInFlight = new Map();
|
|
73
|
+
const DISCOVERY_TTL_MS = 5 * 60 * 1000;
|
|
74
|
+
function roundUsd(value) {
|
|
75
|
+
return Math.round(value * 1000) / 1000;
|
|
76
|
+
}
|
|
77
|
+
function fallbackServiceCatalogEntry(input) {
|
|
78
|
+
const hostname = new URL(input.url).host;
|
|
79
|
+
return {
|
|
80
|
+
serviceId: input.serviceId ?? hostname,
|
|
81
|
+
displayName: input.serviceId ?? hostname,
|
|
82
|
+
description: 'Fallback catalog entry inferred from request URL.',
|
|
83
|
+
baseUrl: `${new URL(input.url).origin}`,
|
|
84
|
+
source: 'fallback',
|
|
85
|
+
capabilities: {
|
|
86
|
+
x402: true,
|
|
87
|
+
mppCharge: true,
|
|
88
|
+
mppSession: true,
|
|
89
|
+
},
|
|
90
|
+
pricing: {
|
|
91
|
+
x402PerCallUsd: 0.01,
|
|
92
|
+
mppChargePerCallUsd: 0.03,
|
|
93
|
+
mppSessionOpenUsd: 0.005,
|
|
94
|
+
mppSessionPerCallUsd: 0.005,
|
|
95
|
+
},
|
|
96
|
+
routingHints: {
|
|
97
|
+
breakEvenCalls: 4,
|
|
98
|
+
streamingPreferred: false,
|
|
99
|
+
preferredSingleCall: 'x402',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function getProjectedRequests(input) {
|
|
104
|
+
return Math.max(1, input.projectedRequests ?? 1);
|
|
105
|
+
}
|
|
106
|
+
function mergeCatalogEntry(base, discovery, input) {
|
|
107
|
+
const modes = new Set(discovery.paymentModes ?? []);
|
|
108
|
+
const pricingHints = discovery.pricingHints ?? {};
|
|
109
|
+
const merged = {
|
|
110
|
+
...base,
|
|
111
|
+
serviceId: discovery.serviceId ?? base.serviceId ?? input.serviceId ?? base.serviceId,
|
|
112
|
+
displayName: base.displayName,
|
|
113
|
+
description: base.description,
|
|
114
|
+
baseUrl: new URL(input.url).origin,
|
|
115
|
+
source: base.source === 'fallback' ? 'discovered' : 'hybrid',
|
|
116
|
+
capabilities: {
|
|
117
|
+
x402: modes.has('x402') || base.capabilities.x402,
|
|
118
|
+
mppCharge: modes.has('mpp-charge') || modes.has('MPP charge') || base.capabilities.mppCharge,
|
|
119
|
+
mppSession: modes.has('mpp-session') || modes.has('MPP session') || base.capabilities.mppSession,
|
|
120
|
+
},
|
|
121
|
+
pricing: {
|
|
122
|
+
x402PerCallUsd: typeof pricingHints.x402PerCallUsd === 'number'
|
|
123
|
+
? pricingHints.x402PerCallUsd
|
|
124
|
+
: typeof pricingHints.perCallUsd === 'number' && modes.has('x402')
|
|
125
|
+
? pricingHints.perCallUsd
|
|
126
|
+
: base.pricing.x402PerCallUsd,
|
|
127
|
+
mppChargePerCallUsd: typeof pricingHints.mppChargePerCallUsd === 'number'
|
|
128
|
+
? pricingHints.mppChargePerCallUsd
|
|
129
|
+
: typeof pricingHints.perCallUsd === 'number' && (modes.has('mpp-charge') || modes.has('MPP charge'))
|
|
130
|
+
? pricingHints.perCallUsd
|
|
131
|
+
: base.pricing.mppChargePerCallUsd,
|
|
132
|
+
mppSessionOpenUsd: typeof pricingHints.mppSessionOpenUsd === 'number'
|
|
133
|
+
? pricingHints.mppSessionOpenUsd
|
|
134
|
+
: typeof pricingHints.sessionOpenUsd === 'number'
|
|
135
|
+
? pricingHints.sessionOpenUsd
|
|
136
|
+
: base.pricing.mppSessionOpenUsd,
|
|
137
|
+
mppSessionPerCallUsd: typeof pricingHints.mppSessionPerCallUsd === 'number'
|
|
138
|
+
? pricingHints.mppSessionPerCallUsd
|
|
139
|
+
: typeof pricingHints.perCallUsd === 'number' &&
|
|
140
|
+
(modes.has('mpp-session') || modes.has('MPP session'))
|
|
141
|
+
? pricingHints.perCallUsd
|
|
142
|
+
: base.pricing.mppSessionPerCallUsd,
|
|
143
|
+
},
|
|
144
|
+
routingHints: {
|
|
145
|
+
breakEvenCalls: typeof pricingHints.breakEvenRequests === 'number'
|
|
146
|
+
? Math.max(1, Math.round(pricingHints.breakEvenRequests))
|
|
147
|
+
: base.routingHints.breakEvenCalls,
|
|
148
|
+
streamingPreferred: typeof pricingHints.streamingPreferred === 'boolean'
|
|
149
|
+
? pricingHints.streamingPreferred
|
|
150
|
+
: base.routingHints.streamingPreferred,
|
|
151
|
+
preferredSingleCall: pricingHints.preferredSingleCall === 'mpp-charge'
|
|
152
|
+
? 'mpp-charge'
|
|
153
|
+
: pricingHints.preferredSingleCall === 'x402'
|
|
154
|
+
? 'x402'
|
|
155
|
+
: base.routingHints.preferredSingleCall,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
return merged;
|
|
159
|
+
}
|
|
160
|
+
export function resolveCatalogEntry(input) {
|
|
161
|
+
const discovered = (input.serviceId ? discoveredCatalog.get(input.serviceId) : undefined) ??
|
|
162
|
+
[...discoveredCatalog.values()].find((entry) => input.url.startsWith(entry.baseUrl));
|
|
163
|
+
if (discovered) {
|
|
164
|
+
return discovered;
|
|
165
|
+
}
|
|
166
|
+
return ((input.serviceId ? catalogByServiceId.get(input.serviceId) : undefined) ??
|
|
167
|
+
serviceCatalog.find((entry) => input.url.startsWith(entry.baseUrl)) ??
|
|
168
|
+
fallbackServiceCatalogEntry(input));
|
|
169
|
+
}
|
|
170
|
+
async function discoverCatalogEntry(input) {
|
|
171
|
+
const existing = resolveCatalogEntry(input);
|
|
172
|
+
if (existing.source !== 'fallback') {
|
|
173
|
+
return existing;
|
|
174
|
+
}
|
|
175
|
+
const cacheKey = input.serviceId ?? new URL(input.url).origin;
|
|
176
|
+
const cachedAt = discoveryTimestamps.get(cacheKey);
|
|
177
|
+
if (cachedAt && Date.now() - cachedAt < DISCOVERY_TTL_MS) {
|
|
178
|
+
return existing;
|
|
179
|
+
}
|
|
180
|
+
const inflight = discoveryInFlight.get(cacheKey);
|
|
181
|
+
if (inflight) {
|
|
182
|
+
return (await inflight) ?? existing;
|
|
183
|
+
}
|
|
184
|
+
const discoveryPromise = (async () => {
|
|
185
|
+
try {
|
|
186
|
+
const response = await fetch(new URL('/.well-known/xmpp.json', input.url));
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const discovery = (await response.json());
|
|
191
|
+
const merged = mergeCatalogEntry(existing, discovery, input);
|
|
192
|
+
discoveredCatalog.set(merged.serviceId, merged);
|
|
193
|
+
discoveryTimestamps.set(cacheKey, Date.now());
|
|
194
|
+
return merged;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
discoveryInFlight.delete(cacheKey);
|
|
201
|
+
}
|
|
202
|
+
})();
|
|
203
|
+
discoveryInFlight.set(cacheKey, discoveryPromise);
|
|
204
|
+
return (await discoveryPromise) ?? existing;
|
|
205
|
+
}
|
|
206
|
+
function getNaiveX402Cost(entry, projectedRequests) {
|
|
207
|
+
return projectedRequests * entry.pricing.x402PerCallUsd;
|
|
208
|
+
}
|
|
209
|
+
function getEstimatedCost(route, entry, projectedRequests, hasReusableSession = false) {
|
|
210
|
+
if (route === 'x402') {
|
|
211
|
+
return getNaiveX402Cost(entry, projectedRequests);
|
|
212
|
+
}
|
|
213
|
+
if (route === 'mpp-charge') {
|
|
214
|
+
return projectedRequests * entry.pricing.mppChargePerCallUsd;
|
|
215
|
+
}
|
|
216
|
+
const openCost = hasReusableSession || route === 'mpp-session-reuse' ? 0 : entry.pricing.mppSessionOpenUsd;
|
|
217
|
+
return openCost + projectedRequests * entry.pricing.mppSessionPerCallUsd;
|
|
218
|
+
}
|
|
219
|
+
export function estimateRouteCost(input) {
|
|
220
|
+
const context = normalizeContext(input);
|
|
221
|
+
const entry = resolveCatalogEntry(context);
|
|
222
|
+
return roundUsd(getEstimatedCost(input.route, entry, getProjectedRequests(context), input.hasReusableSession));
|
|
223
|
+
}
|
|
224
|
+
function supportsRoute(entry, route) {
|
|
225
|
+
if (route === 'x402') {
|
|
226
|
+
return entry.capabilities.x402;
|
|
227
|
+
}
|
|
228
|
+
if (route === 'mpp-charge') {
|
|
229
|
+
return entry.capabilities.mppCharge;
|
|
230
|
+
}
|
|
231
|
+
return entry.capabilities.mppSession;
|
|
232
|
+
}
|
|
233
|
+
function scoreRoute(input) {
|
|
234
|
+
const { entry, route, projectedRequests, streaming, hasReusableSession } = input;
|
|
235
|
+
const supported = supportsRoute(entry, route);
|
|
236
|
+
const estimatedTotalUsd = roundUsd(getEstimatedCost(route, entry, projectedRequests, hasReusableSession));
|
|
237
|
+
const naiveX402Cost = roundUsd(getNaiveX402Cost(entry, projectedRequests));
|
|
238
|
+
const savingsVsNaiveUsd = roundUsd(naiveX402Cost - estimatedTotalUsd);
|
|
239
|
+
const reasons = [];
|
|
240
|
+
let totalScore = supported ? 0.2 : -1;
|
|
241
|
+
if (!supported) {
|
|
242
|
+
reasons.push('Service catalog does not advertise this payment mode.');
|
|
243
|
+
return {
|
|
244
|
+
route,
|
|
245
|
+
supported,
|
|
246
|
+
estimatedTotalUsd,
|
|
247
|
+
savingsVsNaiveUsd,
|
|
248
|
+
totalScore,
|
|
249
|
+
reasons,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const costDeltaScore = Math.max(-0.2, Math.min(0.45, savingsVsNaiveUsd * 8));
|
|
253
|
+
totalScore += costDeltaScore;
|
|
254
|
+
if (costDeltaScore > 0) {
|
|
255
|
+
reasons.push(`Estimated ${route} cost is $${estimatedTotalUsd.toFixed(3)}, saving about $${Math.abs(savingsVsNaiveUsd).toFixed(3)} versus naive x402.`);
|
|
256
|
+
}
|
|
257
|
+
else if (costDeltaScore < 0) {
|
|
258
|
+
reasons.push(`Estimated ${route} cost is higher than x402 for this request shape.`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
reasons.push('Estimated protocol cost is roughly neutral against naive x402.');
|
|
262
|
+
}
|
|
263
|
+
if (route === 'x402') {
|
|
264
|
+
if (projectedRequests === 1 && !streaming) {
|
|
265
|
+
totalScore += 0.28;
|
|
266
|
+
reasons.push('Exact settlement fits a single low-friction call.');
|
|
267
|
+
}
|
|
268
|
+
if (entry.routingHints.preferredSingleCall === 'x402') {
|
|
269
|
+
totalScore += 0.1;
|
|
270
|
+
reasons.push('Service catalog marks x402 as the preferred one-shot route.');
|
|
271
|
+
}
|
|
272
|
+
if (projectedRequests >= entry.routingHints.breakEvenCalls) {
|
|
273
|
+
totalScore -= 0.18;
|
|
274
|
+
reasons.push('Projected repeat volume weakens x402 because fees do not amortize.');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (route === 'mpp-charge') {
|
|
278
|
+
if (projectedRequests === 1) {
|
|
279
|
+
totalScore += 0.2;
|
|
280
|
+
reasons.push('MPP charge fits premium one-shot HTTP payment auth.');
|
|
281
|
+
}
|
|
282
|
+
if (entry.routingHints.preferredSingleCall === 'mpp-charge') {
|
|
283
|
+
totalScore += 0.14;
|
|
284
|
+
reasons.push('Service catalog marks MPP charge as the preferred single-call route.');
|
|
285
|
+
}
|
|
286
|
+
if (projectedRequests >= entry.routingHints.breakEvenCalls) {
|
|
287
|
+
totalScore -= 0.08;
|
|
288
|
+
reasons.push('Repeated calls reduce the relative advantage of one-shot charge flows.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (route === 'mpp-session-open' || route === 'mpp-session-reuse') {
|
|
292
|
+
if (streaming || entry.routingHints.streamingPreferred) {
|
|
293
|
+
totalScore += 0.24;
|
|
294
|
+
reasons.push('Streaming or repeated access strongly favors session economics.');
|
|
295
|
+
}
|
|
296
|
+
if (projectedRequests >= entry.routingHints.breakEvenCalls) {
|
|
297
|
+
totalScore += 0.18;
|
|
298
|
+
reasons.push(`Projected usage crosses the ${entry.routingHints.breakEvenCalls}-call break-even point.`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
totalScore -= 0.1;
|
|
302
|
+
reasons.push('Projected usage is below the session break-even threshold.');
|
|
303
|
+
}
|
|
304
|
+
if (route === 'mpp-session-reuse' || hasReusableSession) {
|
|
305
|
+
totalScore += 0.16;
|
|
306
|
+
reasons.push('Reusable session removes the channel open cost.');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
reasons.push('First session call still carries the channel open cost.');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
route,
|
|
314
|
+
supported,
|
|
315
|
+
estimatedTotalUsd,
|
|
316
|
+
savingsVsNaiveUsd,
|
|
317
|
+
totalScore: roundUsd(totalScore),
|
|
318
|
+
reasons,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function sortBreakdown(left, right) {
|
|
322
|
+
if (right.totalScore !== left.totalScore) {
|
|
323
|
+
return right.totalScore - left.totalScore;
|
|
324
|
+
}
|
|
325
|
+
return left.estimatedTotalUsd - right.estimatedTotalUsd;
|
|
326
|
+
}
|
|
327
|
+
function chooseBestDecision(entry, projectedRequests, breakdown) {
|
|
328
|
+
const best = [...breakdown].sort(sortBreakdown)[0];
|
|
329
|
+
return {
|
|
330
|
+
route: best.route,
|
|
331
|
+
reason: best.reasons[0] ?? 'xMPP selected the highest scoring route.',
|
|
332
|
+
score: best.totalScore,
|
|
333
|
+
projectedRequests,
|
|
334
|
+
estimatedTotalUsd: best.estimatedTotalUsd,
|
|
335
|
+
savingsVsNaiveUsd: best.savingsVsNaiveUsd,
|
|
336
|
+
service: entry,
|
|
337
|
+
breakdown: [...breakdown].sort(sortBreakdown),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function explainPreview(input) {
|
|
341
|
+
const entry = resolveCatalogEntry(input);
|
|
342
|
+
const projectedRequests = getProjectedRequests(input);
|
|
343
|
+
const streaming = Boolean(input.streaming);
|
|
344
|
+
const breakdown = [
|
|
345
|
+
'x402',
|
|
346
|
+
'mpp-charge',
|
|
347
|
+
input.hasReusableSession ? 'mpp-session-reuse' : 'mpp-session-open',
|
|
348
|
+
].map((route) => scoreRoute({
|
|
349
|
+
entry,
|
|
350
|
+
route,
|
|
351
|
+
projectedRequests,
|
|
352
|
+
streaming,
|
|
353
|
+
hasReusableSession: input.hasReusableSession,
|
|
354
|
+
}));
|
|
355
|
+
return chooseBestDecision(entry, projectedRequests, breakdown);
|
|
356
|
+
}
|
|
357
|
+
export function getServiceCatalog() {
|
|
358
|
+
const merged = new Map(serviceCatalog.map((entry) => [entry.serviceId, entry]));
|
|
359
|
+
for (const entry of discoveredCatalog.values()) {
|
|
360
|
+
merged.set(entry.serviceId, entry);
|
|
361
|
+
}
|
|
362
|
+
return [...merged.values()];
|
|
363
|
+
}
|
|
364
|
+
export function getServiceCatalogEntry(serviceId) {
|
|
365
|
+
return discoveredCatalog.get(serviceId) ?? catalogByServiceId.get(serviceId) ?? null;
|
|
366
|
+
}
|
|
367
|
+
function normalizeContext(input) {
|
|
368
|
+
return {
|
|
369
|
+
...input,
|
|
370
|
+
method: input.method ?? 'GET',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
export function createRouter() {
|
|
374
|
+
return {
|
|
375
|
+
async preview(input) {
|
|
376
|
+
await discoverCatalogEntry(input);
|
|
377
|
+
return explainPreview(input);
|
|
378
|
+
},
|
|
379
|
+
async chooseFromChallenge(input) {
|
|
380
|
+
await discoverCatalogEntry(input);
|
|
381
|
+
if (input.challenge.kind === 'mpp-session') {
|
|
382
|
+
return explainPreview({
|
|
383
|
+
...input,
|
|
384
|
+
hasReusableSession: input.hasReusableSession,
|
|
385
|
+
streaming: input.streaming ?? true,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (input.challenge.kind === 'mpp-charge') {
|
|
389
|
+
const decision = explainPreview(input);
|
|
390
|
+
return {
|
|
391
|
+
...decision,
|
|
392
|
+
route: 'mpp-charge',
|
|
393
|
+
reason: 'MPP charge challenge detected and the service is requesting one-shot auth.',
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const decision = explainPreview(input);
|
|
397
|
+
return {
|
|
398
|
+
...decision,
|
|
399
|
+
route: 'x402',
|
|
400
|
+
reason: 'x402 challenge detected and exact settlement is available immediately.',
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
explain(input) {
|
|
404
|
+
return explainPreview(input);
|
|
405
|
+
},
|
|
406
|
+
estimateWorkflow(steps) {
|
|
407
|
+
const breakdown = steps.map((step) => {
|
|
408
|
+
const decision = explainPreview(normalizeContext(step));
|
|
409
|
+
return {
|
|
410
|
+
serviceId: decision.service?.serviceId ?? step.serviceId ?? new URL(step.url).host,
|
|
411
|
+
displayName: decision.service?.displayName ?? step.serviceId ?? new URL(step.url).host,
|
|
412
|
+
route: decision.route,
|
|
413
|
+
projectedRequests: Math.max(1, step.projectedRequests),
|
|
414
|
+
estimatedCostUsd: roundUsd(decision.estimatedTotalUsd ?? 0),
|
|
415
|
+
savingsVsNaiveUsd: roundUsd(decision.savingsVsNaiveUsd ?? 0),
|
|
416
|
+
reason: decision.reason,
|
|
417
|
+
};
|
|
418
|
+
});
|
|
419
|
+
const totalEstimatedCostUsd = roundUsd(breakdown.reduce((sum, item) => sum + item.estimatedCostUsd, 0));
|
|
420
|
+
const naiveX402CostUsd = roundUsd(steps.reduce((sum, step) => {
|
|
421
|
+
const entry = resolveCatalogEntry(normalizeContext(step));
|
|
422
|
+
return sum + getNaiveX402Cost(entry, Math.max(1, step.projectedRequests));
|
|
423
|
+
}, 0));
|
|
424
|
+
return {
|
|
425
|
+
totalEstimatedCostUsd,
|
|
426
|
+
naiveX402CostUsd,
|
|
427
|
+
savingsVsNaiveUsd: roundUsd(naiveX402CostUsd - totalEstimatedCostUsd),
|
|
428
|
+
breakdown,
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createRouter, getServiceCatalogEntry } from './index.js';
|
|
3
|
+
describe('router', () => {
|
|
4
|
+
const router = createRouter();
|
|
5
|
+
it('prefers x402 for low-volume calls', async () => {
|
|
6
|
+
const result = await router.preview({
|
|
7
|
+
url: 'http://localhost:4101/research?q=stellar',
|
|
8
|
+
method: 'GET',
|
|
9
|
+
projectedRequests: 1,
|
|
10
|
+
serviceId: 'research-api',
|
|
11
|
+
});
|
|
12
|
+
expect(result.route).toBe('x402');
|
|
13
|
+
expect(result.service?.serviceId).toBe('research-api');
|
|
14
|
+
expect(result.breakdown?.[0]?.route).toBe('x402');
|
|
15
|
+
});
|
|
16
|
+
it('prefers mpp charge for market-api single calls', async () => {
|
|
17
|
+
const result = await router.preview({
|
|
18
|
+
url: 'http://localhost:4102/quote?symbol=XLM',
|
|
19
|
+
method: 'GET',
|
|
20
|
+
projectedRequests: 1,
|
|
21
|
+
serviceId: 'market-api',
|
|
22
|
+
});
|
|
23
|
+
expect(result.route).toBe('mpp-charge');
|
|
24
|
+
expect(result.reason.toLowerCase()).toContain('saving');
|
|
25
|
+
});
|
|
26
|
+
it('prefers mpp session for repeated calls', async () => {
|
|
27
|
+
const result = await router.preview({
|
|
28
|
+
url: 'http://localhost:4103/stream/tick',
|
|
29
|
+
method: 'GET',
|
|
30
|
+
projectedRequests: 5,
|
|
31
|
+
serviceId: 'stream-api',
|
|
32
|
+
streaming: true,
|
|
33
|
+
});
|
|
34
|
+
expect(result.route).toBe('mpp-session-open');
|
|
35
|
+
expect(result.breakdown?.find((item) => item.route === 'mpp-session-open')?.supported).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it('promotes reusable session when one already exists', async () => {
|
|
38
|
+
const result = router.explain({
|
|
39
|
+
url: 'http://localhost:4103/stream/tick',
|
|
40
|
+
method: 'GET',
|
|
41
|
+
projectedRequests: 5,
|
|
42
|
+
serviceId: 'stream-api',
|
|
43
|
+
streaming: true,
|
|
44
|
+
hasReusableSession: true,
|
|
45
|
+
});
|
|
46
|
+
expect(result.route).toBe('mpp-session-reuse');
|
|
47
|
+
});
|
|
48
|
+
it('estimates a mixed workflow against naive x402 spend', () => {
|
|
49
|
+
const estimate = router.estimateWorkflow([
|
|
50
|
+
{
|
|
51
|
+
url: 'http://localhost:4101/research?q=stellar',
|
|
52
|
+
serviceId: 'research-api',
|
|
53
|
+
projectedRequests: 1,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
url: 'http://localhost:4102/quote?symbol=XLM',
|
|
57
|
+
serviceId: 'market-api',
|
|
58
|
+
projectedRequests: 1,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
url: 'http://localhost:4103/stream/tick',
|
|
62
|
+
serviceId: 'stream-api',
|
|
63
|
+
projectedRequests: 5,
|
|
64
|
+
streaming: true,
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
expect(estimate.breakdown).toHaveLength(3);
|
|
68
|
+
expect(estimate.totalEstimatedCostUsd).toBeGreaterThan(0);
|
|
69
|
+
expect(estimate.savingsVsNaiveUsd).toBeGreaterThanOrEqual(0);
|
|
70
|
+
});
|
|
71
|
+
it('exposes catalog entries for docs and operator views', () => {
|
|
72
|
+
const entry = getServiceCatalogEntry('market-api');
|
|
73
|
+
expect(entry?.displayName).toBe('Market API');
|
|
74
|
+
expect(entry?.capabilities.mppCharge).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
export type RouteKind = 'x402' | 'mpp-charge' | 'mpp-session-open' | 'mpp-session-reuse';
|
|
2
|
+
export type PaymentExecutionMode = 'mock' | 'testnet';
|
|
3
|
+
export type PaymentExecutionStatus = 'mock-paid' | 'ready-for-testnet' | 'settled-testnet' | 'missing-config';
|
|
4
|
+
export type RouteContext = {
|
|
5
|
+
url: string;
|
|
6
|
+
method: string;
|
|
7
|
+
serviceId?: string;
|
|
8
|
+
projectedRequests?: number;
|
|
9
|
+
streaming?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type ServiceCatalogEntry = {
|
|
12
|
+
serviceId: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
description: string;
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
source?: 'static' | 'discovered' | 'hybrid' | 'fallback';
|
|
17
|
+
capabilities: {
|
|
18
|
+
x402: boolean;
|
|
19
|
+
mppCharge: boolean;
|
|
20
|
+
mppSession: boolean;
|
|
21
|
+
};
|
|
22
|
+
pricing: {
|
|
23
|
+
x402PerCallUsd: number;
|
|
24
|
+
mppChargePerCallUsd: number;
|
|
25
|
+
mppSessionOpenUsd: number;
|
|
26
|
+
mppSessionPerCallUsd: number;
|
|
27
|
+
};
|
|
28
|
+
routingHints: {
|
|
29
|
+
breakEvenCalls: number;
|
|
30
|
+
streamingPreferred: boolean;
|
|
31
|
+
preferredSingleCall: Extract<RouteKind, 'x402' | 'mpp-charge'>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type RouteScoreBreakdown = {
|
|
35
|
+
route: RouteKind;
|
|
36
|
+
supported: boolean;
|
|
37
|
+
estimatedTotalUsd: number;
|
|
38
|
+
savingsVsNaiveUsd: number;
|
|
39
|
+
totalScore: number;
|
|
40
|
+
reasons: string[];
|
|
41
|
+
};
|
|
42
|
+
export type RouteDecision = {
|
|
43
|
+
route: RouteKind;
|
|
44
|
+
reason: string;
|
|
45
|
+
score: number;
|
|
46
|
+
projectedRequests?: number;
|
|
47
|
+
estimatedTotalUsd?: number;
|
|
48
|
+
savingsVsNaiveUsd?: number;
|
|
49
|
+
service?: ServiceCatalogEntry;
|
|
50
|
+
breakdown?: RouteScoreBreakdown[];
|
|
51
|
+
};
|
|
52
|
+
export type ChallengeKind = 'x402' | 'mpp-charge' | 'mpp-session';
|
|
53
|
+
export type PaymentChallenge = {
|
|
54
|
+
kind: ChallengeKind;
|
|
55
|
+
service: string;
|
|
56
|
+
amountUsd: number;
|
|
57
|
+
asset: 'USDC_TESTNET';
|
|
58
|
+
retryHeaderName: string;
|
|
59
|
+
retryHeaderValue: string;
|
|
60
|
+
sessionId?: string;
|
|
61
|
+
};
|
|
62
|
+
export type XmppFetchOptions = Omit<RouteContext, 'url' | 'method'> & {
|
|
63
|
+
agentId?: string;
|
|
64
|
+
maxAutoPayUsd?: number;
|
|
65
|
+
idempotencyKey?: string;
|
|
66
|
+
};
|
|
67
|
+
export type XmppAgentProfile = {
|
|
68
|
+
agentId: string;
|
|
69
|
+
displayName: string;
|
|
70
|
+
role: 'shared' | 'research' | 'market';
|
|
71
|
+
description: string;
|
|
72
|
+
dailyBudgetUsd: number;
|
|
73
|
+
allowedServices: string[];
|
|
74
|
+
preferredRoutes: RouteKind[];
|
|
75
|
+
autopayMethods: string[];
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
policySource?: 'local' | 'contract' | 'fallback' | 'merged';
|
|
78
|
+
};
|
|
79
|
+
export type XmppSignedReceipt = {
|
|
80
|
+
receiptId: string;
|
|
81
|
+
issuedAt: string;
|
|
82
|
+
network: string;
|
|
83
|
+
agent: string;
|
|
84
|
+
serviceId: string;
|
|
85
|
+
url: string;
|
|
86
|
+
method: string;
|
|
87
|
+
route: RouteKind;
|
|
88
|
+
amountUsd: number;
|
|
89
|
+
txHash?: string;
|
|
90
|
+
explorerUrl?: string;
|
|
91
|
+
paymentReference?: string;
|
|
92
|
+
signature: string;
|
|
93
|
+
};
|
|
94
|
+
export type XmppSmartAccountExecution = {
|
|
95
|
+
configured: boolean;
|
|
96
|
+
preferred: boolean;
|
|
97
|
+
supported: boolean;
|
|
98
|
+
used: boolean;
|
|
99
|
+
contractId?: string | null;
|
|
100
|
+
fallbackReason?: string;
|
|
101
|
+
};
|
|
102
|
+
export type PaymentExecutionMetadata = {
|
|
103
|
+
mode: PaymentExecutionMode;
|
|
104
|
+
status: PaymentExecutionStatus;
|
|
105
|
+
route: RouteKind;
|
|
106
|
+
receiptId: string;
|
|
107
|
+
missingConfig?: string[];
|
|
108
|
+
evidenceHeaders?: Record<string, string>;
|
|
109
|
+
signedReceipt?: XmppSignedReceipt;
|
|
110
|
+
settlementStrategy?: 'keypair' | 'smart-account' | 'keypair-fallback';
|
|
111
|
+
executionNote?: string;
|
|
112
|
+
feeSponsored?: boolean;
|
|
113
|
+
feeSponsorPublicKey?: string;
|
|
114
|
+
feeBumpPublicKey?: string;
|
|
115
|
+
smartAccount?: XmppSmartAccountExecution;
|
|
116
|
+
};
|
|
117
|
+
export type PolicyDecision = {
|
|
118
|
+
allowed: boolean;
|
|
119
|
+
reason: string;
|
|
120
|
+
code: 'allowed' | 'blocked-domain' | 'blocked-path' | 'blocked-method' | 'blocked-service' | 'blocked-agent' | 'blocked-budget' | 'blocked-idempotency' | 'paused';
|
|
121
|
+
source?: 'local' | 'contract' | 'fallback';
|
|
122
|
+
};
|
|
123
|
+
export type PaymentExecutionResult = {
|
|
124
|
+
response: Response;
|
|
125
|
+
metadata: PaymentExecutionMetadata;
|
|
126
|
+
};
|
|
127
|
+
export type XmppFetchMetadata = {
|
|
128
|
+
route: RouteKind;
|
|
129
|
+
challenge?: PaymentChallenge;
|
|
130
|
+
retried: boolean;
|
|
131
|
+
execution?: PaymentExecutionMetadata;
|
|
132
|
+
policy?: PolicyDecision;
|
|
133
|
+
budget?: XmppBudgetSnapshot;
|
|
134
|
+
idempotentReplay?: boolean;
|
|
135
|
+
};
|
|
136
|
+
export type XmppSessionRecord = {
|
|
137
|
+
sessionId: string;
|
|
138
|
+
serviceId: string;
|
|
139
|
+
agent: string;
|
|
140
|
+
channelContractId: string;
|
|
141
|
+
route: string;
|
|
142
|
+
status: string;
|
|
143
|
+
totalAmountUsdCents: number;
|
|
144
|
+
callCount: number;
|
|
145
|
+
lastReceiptId: string;
|
|
146
|
+
updatedAtLedger: number;
|
|
147
|
+
};
|
|
148
|
+
export type WorkflowEstimateStep = {
|
|
149
|
+
url: string;
|
|
150
|
+
method?: string;
|
|
151
|
+
serviceId?: string;
|
|
152
|
+
projectedRequests: number;
|
|
153
|
+
streaming?: boolean;
|
|
154
|
+
};
|
|
155
|
+
export type WorkflowEstimateLineItem = {
|
|
156
|
+
serviceId: string;
|
|
157
|
+
displayName: string;
|
|
158
|
+
route: RouteKind;
|
|
159
|
+
projectedRequests: number;
|
|
160
|
+
estimatedCostUsd: number;
|
|
161
|
+
savingsVsNaiveUsd: number;
|
|
162
|
+
reason: string;
|
|
163
|
+
};
|
|
164
|
+
export type WorkflowEstimateResult = {
|
|
165
|
+
totalEstimatedCostUsd: number;
|
|
166
|
+
naiveX402CostUsd: number;
|
|
167
|
+
savingsVsNaiveUsd: number;
|
|
168
|
+
breakdown: WorkflowEstimateLineItem[];
|
|
169
|
+
};
|
|
170
|
+
export type XmppBudgetSnapshot = {
|
|
171
|
+
agentId: string;
|
|
172
|
+
agentDisplayName: string;
|
|
173
|
+
agentSpentThisSessionUsd: number;
|
|
174
|
+
agentRemainingDailyBudgetUsd: number;
|
|
175
|
+
spentThisSessionUsd: number;
|
|
176
|
+
remainingDailyBudgetUsd: number;
|
|
177
|
+
callsThisService: number;
|
|
178
|
+
projectedCostIfRepeated5xUsd: number;
|
|
179
|
+
recommendation: string;
|
|
180
|
+
};
|
|
181
|
+
export type XmppRouteEvent = {
|
|
182
|
+
id: string;
|
|
183
|
+
timestamp: string;
|
|
184
|
+
agentId: string;
|
|
185
|
+
url: string;
|
|
186
|
+
method: string;
|
|
187
|
+
serviceId: string;
|
|
188
|
+
route: RouteKind;
|
|
189
|
+
status: 'settled' | 'denied' | 'preview';
|
|
190
|
+
amountUsd: number;
|
|
191
|
+
projectedRequests: number;
|
|
192
|
+
policyCode?: PolicyDecision['code'];
|
|
193
|
+
receiptId?: string;
|
|
194
|
+
txHash?: string;
|
|
195
|
+
explorerUrl?: string;
|
|
196
|
+
sessionId?: string;
|
|
197
|
+
signedReceipt?: XmppSignedReceipt;
|
|
198
|
+
feeSponsored?: boolean;
|
|
199
|
+
feeSponsorPublicKey?: string;
|
|
200
|
+
settlementStrategy?: PaymentExecutionMetadata['settlementStrategy'];
|
|
201
|
+
executionNote?: string;
|
|
202
|
+
};
|
|
203
|
+
export type XmppAgentStateSummary = {
|
|
204
|
+
agentId: string;
|
|
205
|
+
displayName: string;
|
|
206
|
+
role: XmppAgentProfile['role'];
|
|
207
|
+
description: string;
|
|
208
|
+
dailyBudgetUsd: number;
|
|
209
|
+
spentThisSessionUsd: number;
|
|
210
|
+
remainingDailyBudgetUsd: number;
|
|
211
|
+
routeCounts: Record<RouteKind, number>;
|
|
212
|
+
allowedServices: string[];
|
|
213
|
+
preferredRoutes: RouteKind[];
|
|
214
|
+
enabled?: boolean;
|
|
215
|
+
policySource?: XmppAgentProfile['policySource'];
|
|
216
|
+
autopayMethods?: string[];
|
|
217
|
+
};
|
|
218
|
+
export type XmppAgentPolicySnapshot = {
|
|
219
|
+
agentId: string;
|
|
220
|
+
enabled: boolean;
|
|
221
|
+
dailyBudgetUsd: number;
|
|
222
|
+
allowedServices: string[];
|
|
223
|
+
preferredRoutes: RouteKind[];
|
|
224
|
+
autopayMethods: string[];
|
|
225
|
+
source: 'contract' | 'local' | 'fallback';
|
|
226
|
+
};
|
|
227
|
+
export type XmppContractTreasurySnapshot = {
|
|
228
|
+
sharedTreasuryUsd: number;
|
|
229
|
+
totalSpentUsd: number;
|
|
230
|
+
remainingUsd: number;
|
|
231
|
+
paymentCount: number;
|
|
232
|
+
source: 'contract' | 'local' | 'fallback';
|
|
233
|
+
};
|
|
234
|
+
export type XmppContractAgentTreasuryState = {
|
|
235
|
+
agentId: string;
|
|
236
|
+
spentUsd: number;
|
|
237
|
+
paymentCount: number;
|
|
238
|
+
lastServiceId: string;
|
|
239
|
+
lastRoute: string;
|
|
240
|
+
source: 'contract' | 'local' | 'fallback';
|
|
241
|
+
};
|
|
242
|
+
export type XmppOperatorState = {
|
|
243
|
+
sharedTreasuryUsd: number;
|
|
244
|
+
sharedTreasuryRemainingUsd: number;
|
|
245
|
+
dailyBudgetUsd: number;
|
|
246
|
+
spentThisSessionUsd: number;
|
|
247
|
+
remainingDailyBudgetUsd: number;
|
|
248
|
+
sessionSavingsUsd: number;
|
|
249
|
+
routeCounts: Record<RouteKind, number>;
|
|
250
|
+
serviceSpendUsd: Record<string, number>;
|
|
251
|
+
serviceCallCounts: Record<string, number>;
|
|
252
|
+
agentProfiles: XmppAgentProfile[];
|
|
253
|
+
agentStates: XmppAgentStateSummary[];
|
|
254
|
+
contractAgentPolicies?: XmppAgentPolicySnapshot[];
|
|
255
|
+
contractTreasury?: XmppContractTreasurySnapshot | null;
|
|
256
|
+
contractAgentTreasuryStates?: XmppContractAgentTreasuryState[];
|
|
257
|
+
openSessions: Array<Pick<XmppSessionRecord, 'sessionId' | 'serviceId' | 'callCount'>>;
|
|
258
|
+
recentEvents: XmppRouteEvent[];
|
|
259
|
+
};
|
|
260
|
+
export type XmppReceiptVerificationResult = {
|
|
261
|
+
valid: boolean;
|
|
262
|
+
agent: string;
|
|
263
|
+
receiptId: string;
|
|
264
|
+
};
|
|
265
|
+
export type XmppWalletInfo = {
|
|
266
|
+
connected: boolean;
|
|
267
|
+
paymentExecutionMode: PaymentExecutionMode;
|
|
268
|
+
network: string;
|
|
269
|
+
rpcUrl: string;
|
|
270
|
+
agentPublicKey: string | null;
|
|
271
|
+
settlementStrategy: 'smart-account-ready' | 'smart-account-x402-preferred' | 'smart-account-partial-fallback' | 'keypair-live';
|
|
272
|
+
smartAccount: {
|
|
273
|
+
ready: boolean;
|
|
274
|
+
mode: 'inactive' | 'x402-only' | 'full';
|
|
275
|
+
routeCoverage: 'inactive' | 'x402-only';
|
|
276
|
+
demoReady: boolean;
|
|
277
|
+
guardedFallback: boolean;
|
|
278
|
+
contractId: string | null;
|
|
279
|
+
wasmHash: string;
|
|
280
|
+
webauthnVerifierAddress: string;
|
|
281
|
+
ed25519VerifierAddress: string;
|
|
282
|
+
spendingLimitPolicyAddress: string;
|
|
283
|
+
thresholdPolicyAddress: string;
|
|
284
|
+
preferredRoutes: RouteKind[];
|
|
285
|
+
fallbackRoutes: RouteKind[];
|
|
286
|
+
supportedRoutes: RouteKind[];
|
|
287
|
+
unsupportedRoutes: RouteKind[];
|
|
288
|
+
unsupportedReason: string | null;
|
|
289
|
+
configuredMaxTransactionFeeStroops: number;
|
|
290
|
+
effectiveMaxTransactionFeeStroops: number;
|
|
291
|
+
feeFloorApplied: boolean;
|
|
292
|
+
preflightFailures: string[];
|
|
293
|
+
coverageMessage: string;
|
|
294
|
+
message: string;
|
|
295
|
+
operatorNotes: string[];
|
|
296
|
+
};
|
|
297
|
+
feeSponsorship: {
|
|
298
|
+
enabled: boolean;
|
|
299
|
+
available: boolean;
|
|
300
|
+
mppChargeEnabled: boolean;
|
|
301
|
+
mppSessionEnabled: boolean;
|
|
302
|
+
sponsorPublicKey: string | null;
|
|
303
|
+
feeBumpPublicKey: string | null;
|
|
304
|
+
message: string;
|
|
305
|
+
};
|
|
306
|
+
missingSecrets: string[];
|
|
307
|
+
message: string;
|
|
308
|
+
};
|
|
309
|
+
export type XmppHealthStatus = {
|
|
310
|
+
ok: boolean;
|
|
311
|
+
service: string;
|
|
312
|
+
network: string;
|
|
313
|
+
paymentExecutionMode: PaymentExecutionMode;
|
|
314
|
+
services: Record<string, string>;
|
|
315
|
+
smartAccount: {
|
|
316
|
+
configured: boolean;
|
|
317
|
+
routeCoverage: 'inactive' | 'x402-only';
|
|
318
|
+
x402Preferred: boolean;
|
|
319
|
+
mppFallback: boolean;
|
|
320
|
+
demoReady: boolean;
|
|
321
|
+
guardedFallback: boolean;
|
|
322
|
+
unsupportedRoutes: RouteKind[];
|
|
323
|
+
unsupportedReason: string | null;
|
|
324
|
+
configuredMaxTransactionFeeStroops: number;
|
|
325
|
+
effectiveMaxTransactionFeeStroops: number;
|
|
326
|
+
feeFloorApplied: boolean;
|
|
327
|
+
preflightFailures: string[];
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
export type XmppCatalogResponse = {
|
|
331
|
+
services: ServiceCatalogEntry[];
|
|
332
|
+
};
|
|
333
|
+
export type XmppPolicyPreviewResponse = {
|
|
334
|
+
policy: PolicyDecision;
|
|
335
|
+
routePreview: RouteDecision;
|
|
336
|
+
};
|
|
337
|
+
export type XmppGatewayFetchResponse = {
|
|
338
|
+
status: number;
|
|
339
|
+
routePreview: RouteDecision;
|
|
340
|
+
payment?: XmppFetchMetadata;
|
|
341
|
+
responseHeaders: Record<string, string>;
|
|
342
|
+
body: unknown;
|
|
343
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vinaystwt/xmpp-router",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cost-aware route scoring and workflow estimation across x402, MPP charge, and MPP session flows.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/router/src/index.js",
|
|
8
|
+
"types": "dist/router/src/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"homepage": "https://github.com/Vinaystwt/xMPP#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Vinaystwt/xMPP.git",
|
|
18
|
+
"directory": "packages/router"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/Vinaystwt/xMPP/issues"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"xmpp",
|
|
25
|
+
"router",
|
|
26
|
+
"stellar",
|
|
27
|
+
"x402",
|
|
28
|
+
"mpp"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/router/src/index.d.ts",
|
|
33
|
+
"import": "./dist/router/src/index.js",
|
|
34
|
+
"default": "./dist/router/src/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
43
|
+
"lint": "eslint src",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@vinaystwt/xmpp-types": "0.1.0"
|
|
49
|
+
}
|
|
50
|
+
}
|