@parallel-protocol/x402 0.5.3 → 0.6.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 +51 -4
- package/dist/{chunk-KQPIZ6SZ.cjs → chunk-K6QETMQA.cjs} +124 -15
- package/dist/{chunk-75GLDK7L.js → chunk-TLCEPWQI.js} +120 -16
- package/dist/express.cjs +14 -6
- package/dist/express.d.cts +2 -2
- package/dist/express.d.ts +2 -2
- package/dist/express.js +12 -4
- package/dist/fastify.cjs +62 -13
- package/dist/fastify.d.cts +1 -1
- package/dist/fastify.d.ts +1 -1
- package/dist/fastify.js +60 -11
- package/dist/hono.cjs +10 -6
- package/dist/hono.d.cts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/hono.js +8 -4
- package/dist/index.cjs +21 -13
- package/dist/index.d.cts +29 -4
- package/dist/index.d.ts +29 -4
- package/dist/index.js +1 -1
- package/dist/next.cjs +36 -7
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +33 -4
- package/dist/types-CBzm5CtM.d.cts +131 -0
- package/dist/types-CBzm5CtM.d.ts +131 -0
- package/package.json +2 -2
- package/dist/chunk-2UEAVMCH.js +0 -17
- package/dist/chunk-XUNNMCO5.cjs +0 -20
- package/dist/types-CUi55YSx.d.cts +0 -95
- package/dist/types-CUi55YSx.d.ts +0 -95
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ Agent Merchant (this SDK) Facilitator
|
|
|
40
40
|
- The on-chain settlement happens after the handler produces a 2xx response — the agent is not charged on errors.
|
|
41
41
|
- If settlement fails (on-chain revert, facilitator timeout), the middleware returns `402` and discards the handler's response.
|
|
42
42
|
- Handlers are bounded by a 30-second timeout on Express, Hono and Next.js — a slow handler returns `504` and the agent is not charged. (Fastify manages handler execution itself, so no timeout is applied there.)
|
|
43
|
+
- On paid Fastify routes, writing to `reply.raw` (the SSE/streaming pattern) fails loudly instead of sending: a raw response skips the settlement hooks and would go out unpaid. Streaming is not supported on paid routes.
|
|
43
44
|
- CORS preflight (`OPTIONS`) is always passed through without challenge.
|
|
44
45
|
|
|
45
46
|
---
|
|
@@ -198,6 +199,8 @@ interface PaymentMiddlewareConfig {
|
|
|
198
199
|
| `payTo` | `Address` | Yes | The merchant's receiving address. |
|
|
199
200
|
| `acceptedTokens` | `Address[]` | No | List of accepted token addresses. Defaults to `[USDp, USDC, sUSDp]` for known networks (see [Default tokens](#default-tokens)). |
|
|
200
201
|
| `description` | `string` | No | Human-readable description of the resource, included in the 402 body. |
|
|
202
|
+
| `scheme` | `"exact" \| "upto"` | No | `exact` (default) charges `price` as invoiced. `upto` invoices `price` as a **cap** the payer signs; the handler names the actual amount to charge and the rest is refunded on-chain — see [Metered routes](#metered-routes-upto). |
|
|
203
|
+
| `maxTimeoutSeconds` | `number` | No | Settlement window the payer signs for (default 300). Metered routes announce how long their handler may take; the facilitator caps it at 3600 (one hour). Express, Hono and Next.js derive the handler timeout from it; Fastify does not time handlers out, set the server's `connectionTimeout` / `requestTimeout` yourself. |
|
|
201
204
|
|
|
202
205
|
Route matching uses **longest prefix wins**: `/api/v1/users` will match a `/api/v1` route config before a `/api` one.
|
|
203
206
|
|
|
@@ -299,12 +302,12 @@ interface PaymentRequired {
|
|
|
299
302
|
mimeType: "application/json";
|
|
300
303
|
};
|
|
301
304
|
accepts: Array<{ // one entry per accepted token
|
|
302
|
-
scheme: "exact";
|
|
305
|
+
scheme: "exact" | "upto"; // the route's scheme ("exact" unless configured)
|
|
303
306
|
network: string; // EIP-155 format: "eip155:8453"
|
|
304
307
|
asset: string; // token address
|
|
305
|
-
amount: string; // smallest unit of `asset`, per its own decimals
|
|
308
|
+
amount: string; // smallest unit of `asset`, per its own decimals — the cap for "upto"
|
|
306
309
|
payTo: string;
|
|
307
|
-
maxTimeoutSeconds: 300
|
|
310
|
+
maxTimeoutSeconds: number; // the route's settlement window (default 300)
|
|
308
311
|
extra: { decimals: number }; // decimals of `asset` — payers need not guess
|
|
309
312
|
}>;
|
|
310
313
|
}
|
|
@@ -350,6 +353,7 @@ interface PaymentConfirmation {
|
|
|
350
353
|
chain: string; // "base"
|
|
351
354
|
gasSponsored: boolean;
|
|
352
355
|
networkId: string; // "eip155:8453"
|
|
356
|
+
amountSettled?: string; // metered routes: what was actually charged (smallest unit)
|
|
353
357
|
}
|
|
354
358
|
```
|
|
355
359
|
|
|
@@ -357,9 +361,51 @@ The `access-control-expose-headers` header is set automatically so browsers can
|
|
|
357
361
|
|
|
358
362
|
---
|
|
359
363
|
|
|
364
|
+
## Metered routes (`upto`)
|
|
365
|
+
|
|
366
|
+
Some resources only know their cost after the work is done — an LLM completion is priced by the tokens it generates. A route declared with `scheme: "upto"` invoices `price` as a **cap**: the payer signs an authorization for that cap, the handler serves the request and names the amount it actually charges, and the facilitator settles that amount through Parallel's escrow contract, refunding the remainder to the payer in the same transaction. Metered payments are made in USDp.
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
routes: {
|
|
370
|
+
"/v1/chat/completions": {
|
|
371
|
+
scheme: "upto",
|
|
372
|
+
price: "2", // the cap, in USDp
|
|
373
|
+
maxTimeoutSeconds: 600, // the handler may take up to ~9 minutes
|
|
374
|
+
network: "base",
|
|
375
|
+
payTo: "0xYourAddress",
|
|
376
|
+
acceptedTokens: [PARALLEL_TOKENS.base.usdp],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The handler reports the amount to charge through the `payment-settle-amount` response header, in whole token units like `price`:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { SETTLE_AMOUNT_HEADER } from "@parallel-protocol/x402";
|
|
385
|
+
|
|
386
|
+
fastify.post("/v1/chat/completions", async (request, reply) => {
|
|
387
|
+
const completion = await runInference(request.body);
|
|
388
|
+
reply.header(SETTLE_AMOUNT_HEADER, completion.costInUsdp); // e.g. "0.0137"
|
|
389
|
+
return completion;
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The middleware strips the header before the response leaves, whatever the status code: a handler that prices the request and then fails never tells the client what it was about to charge. The value must follow the same format as `price` (a plain decimal such as `"0.0137"`, no leading zeros on the integer part, no exponent, no unit) and must not carry more decimals than the token has. Rules on a `2xx` response:
|
|
394
|
+
|
|
395
|
+
| Header value | What happens |
|
|
396
|
+
|---|---|
|
|
397
|
+
| absent | The cap is charged. |
|
|
398
|
+
| `"0"` (an explicit zero) | The request is served for free — nothing is settled, no `payment-response`. |
|
|
399
|
+
| a decimal up to `price` | That amount is settled; the receipt's `amountSettled` carries it. |
|
|
400
|
+
| malformed, more decimals than the token, or above `price` | The response is discarded and the client gets 402 `INVALID_SETTLE_AMOUNT` — a handler bug never charges the payer, and a positive amount the token cannot represent is never silently served for free. Round your cost to the token's precision before setting the header. |
|
|
401
|
+
|
|
402
|
+
Timing: the payer's authorization expires after `maxTimeoutSeconds`, and settlement needs a minute at most. On Express, Hono and Next.js the handler timeout is raised to `maxTimeoutSeconds − 60 s` for metered routes; Fastify handlers are never timed out by the middleware, so size the server's own `connectionTimeout` and `requestTimeout` to the window. A handler that finishes after the window is served but cannot be settled: the client gets 402 `PAYMENT_SETTLEMENT_FAILED` and the merchant has done the work for nothing — keep a margin.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
360
406
|
## Payment routes
|
|
361
407
|
|
|
362
|
-
The facilitator supports
|
|
408
|
+
The facilitator supports 13 settlement routes. The route is selected automatically based on the signed payload:
|
|
363
409
|
|
|
364
410
|
| Route | Credential method(s) | tokenIn (agent pays) | tokenOut (merchant receives) | Gas est. | Notes |
|
|
365
411
|
|-------|--------------------------|---------------------|------------------------------|----------|-------|
|
|
@@ -375,6 +421,7 @@ The facilitator supports 12 settlement routes. The route is selected automatical
|
|
|
375
421
|
| **J** | `transferWithAuthorization` | sUSDp | sUSDp | ~65K | Direct sUSDp share transfer |
|
|
376
422
|
| **K** | `redeemWithAuthorization` + `swapExactOutputWithAuthorization` | sUSDp | non-USDC backing (exact) | ~180K | Same as I but for other Parallelizer collaterals (atomic multicall3) |
|
|
377
423
|
| **L** | `partialRedeemWithAuthorization` | sUSDp (partial) + USDp | USDp or backing collateral | ~180K | Partial sUSDp redeem combined with existing USDp; delivers to merchant (atomic multicall3) |
|
|
424
|
+
| **U** | `uptoWithAuthorization` | USDp (cap) | USDp (actual) | ~130K | Metered payment: the escrow pulls the signed cap, pays the merchant the settled amount and refunds the rest atomically (scheme `upto`) |
|
|
378
425
|
|
|
379
426
|
**Chain requirements:**
|
|
380
427
|
- Routes B, D, E, G, I, K, L require the **Parallelizer** to be deployed on the target chain (Ethereum, Base, Avalanche, HyperEVM).
|
|
@@ -82,13 +82,19 @@ var facilitatorConfigSchema = zod.z.object({
|
|
|
82
82
|
url: zod.z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
|
|
83
83
|
apiKey: zod.z.string().optional()
|
|
84
84
|
});
|
|
85
|
+
var UPTO_MAX_TIMEOUT_SECONDS = 3600;
|
|
85
86
|
var routeConfigSchema = zod.z.object({
|
|
86
87
|
price: priceSchema,
|
|
87
88
|
network: zod.z.string().min(1, "Network is required"),
|
|
88
89
|
payTo: ethereumAddressSchema,
|
|
89
90
|
acceptedTokens: zod.z.array(ethereumAddressSchema).optional(),
|
|
90
|
-
description: zod.z.string().optional()
|
|
91
|
-
|
|
91
|
+
description: zod.z.string().optional(),
|
|
92
|
+
scheme: zod.z.enum(["exact", "upto"]).optional(),
|
|
93
|
+
maxTimeoutSeconds: zod.z.number().int().positive().optional()
|
|
94
|
+
}).refine(
|
|
95
|
+
(route) => route.scheme !== "upto" || (route.maxTimeoutSeconds ?? 300) <= UPTO_MAX_TIMEOUT_SECONDS,
|
|
96
|
+
`maxTimeoutSeconds must be at most ${UPTO_MAX_TIMEOUT_SECONDS} on an upto route`
|
|
97
|
+
);
|
|
92
98
|
var paymentMiddlewareConfigSchema = zod.z.object({
|
|
93
99
|
facilitator: facilitatorConfigSchema,
|
|
94
100
|
routes: zod.z.record(zod.z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
|
|
@@ -100,7 +106,8 @@ var facilitatorResponseSchema = zod.z.discriminatedUnion("success", [
|
|
|
100
106
|
route: zod.z.string(),
|
|
101
107
|
chain: zod.z.string(),
|
|
102
108
|
gasSponsored: zod.z.boolean(),
|
|
103
|
-
networkId: zod.z.string()
|
|
109
|
+
networkId: zod.z.string(),
|
|
110
|
+
amountSettled: zod.z.string().optional()
|
|
104
111
|
}),
|
|
105
112
|
zod.z.object({
|
|
106
113
|
success: zod.z.literal(false),
|
|
@@ -117,7 +124,9 @@ var verifyResponseSchema = zod.z.object({
|
|
|
117
124
|
var settleSuccessSchema = zod.z.object({
|
|
118
125
|
txHash: txHashSchema,
|
|
119
126
|
route: zod.z.string(),
|
|
120
|
-
gasSponsored: zod.z.boolean()
|
|
127
|
+
gasSponsored: zod.z.boolean(),
|
|
128
|
+
// Metered routes: the amount actually charged (smallest unit).
|
|
129
|
+
amountSettled: zod.z.string().optional()
|
|
121
130
|
});
|
|
122
131
|
var settleErrorSchema = zod.z.object({
|
|
123
132
|
error: zod.z.string(),
|
|
@@ -169,14 +178,21 @@ var FacilitatorClient = class {
|
|
|
169
178
|
throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
|
|
170
179
|
}
|
|
171
180
|
}
|
|
172
|
-
async settle(paymentHeader, paymentRequirements) {
|
|
181
|
+
async settle(paymentHeader, paymentRequirements, options) {
|
|
173
182
|
const paymentPayload = decodePaymentHeader(paymentHeader);
|
|
174
183
|
let json;
|
|
175
184
|
try {
|
|
176
185
|
const response = await fetch(`${this.config.url}/x402/settle`, {
|
|
177
186
|
method: "POST",
|
|
178
187
|
headers: this.buildHeaders(),
|
|
179
|
-
|
|
188
|
+
// settleAmount only travels for metered routes — exact bodies are unchanged.
|
|
189
|
+
body: JSON.stringify({
|
|
190
|
+
paymentPayload,
|
|
191
|
+
paymentRequirements,
|
|
192
|
+
...options?.amount !== void 0 && {
|
|
193
|
+
settleAmount: options.amount
|
|
194
|
+
}
|
|
195
|
+
}),
|
|
180
196
|
signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
|
|
181
197
|
});
|
|
182
198
|
if (!response.ok) {
|
|
@@ -199,7 +215,10 @@ var FacilitatorClient = class {
|
|
|
199
215
|
route: parsed.route,
|
|
200
216
|
chain: paymentRequirements.network,
|
|
201
217
|
gasSponsored: parsed.gasSponsored,
|
|
202
|
-
networkId: toEip155Network(paymentRequirements.network)
|
|
218
|
+
networkId: toEip155Network(paymentRequirements.network),
|
|
219
|
+
...parsed.amountSettled !== void 0 && {
|
|
220
|
+
amountSettled: parsed.amountSettled
|
|
221
|
+
}
|
|
203
222
|
};
|
|
204
223
|
}
|
|
205
224
|
return {
|
|
@@ -244,6 +263,22 @@ var FacilitatorClient = class {
|
|
|
244
263
|
return headers;
|
|
245
264
|
}
|
|
246
265
|
};
|
|
266
|
+
|
|
267
|
+
// src/timeout.ts
|
|
268
|
+
var HANDLER_TIMEOUT_MS = 3e4;
|
|
269
|
+
var HANDLER_TIMEOUT = /* @__PURE__ */ Symbol("x402:handler-timeout");
|
|
270
|
+
async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
|
|
271
|
+
let timer;
|
|
272
|
+
const timeout = new Promise((resolve) => {
|
|
273
|
+
timer = setTimeout(() => resolve(HANDLER_TIMEOUT), ms);
|
|
274
|
+
timer.unref?.();
|
|
275
|
+
});
|
|
276
|
+
try {
|
|
277
|
+
return await Promise.race([promise, timeout]);
|
|
278
|
+
} finally {
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
247
282
|
var PARALLEL_TOKENS = {
|
|
248
283
|
ethereum: {
|
|
249
284
|
usdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
|
|
@@ -285,6 +320,34 @@ function getTokenDecimals(network, address) {
|
|
|
285
320
|
}
|
|
286
321
|
|
|
287
322
|
// src/middleware.ts
|
|
323
|
+
var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
|
|
324
|
+
var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
|
|
325
|
+
function handlerTimeoutFor(config) {
|
|
326
|
+
if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
|
|
327
|
+
const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
|
|
328
|
+
return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
|
|
329
|
+
}
|
|
330
|
+
function resolveSettleAmount(raw, maxAmount, decimals) {
|
|
331
|
+
if (raw === void 0)
|
|
332
|
+
return { kind: "settle", amount: maxAmount.toString() };
|
|
333
|
+
const value = raw.trim();
|
|
334
|
+
let amount;
|
|
335
|
+
try {
|
|
336
|
+
amount = parsePrice(value, decimals);
|
|
337
|
+
} catch {
|
|
338
|
+
return { kind: "invalid", reason: "malformed" };
|
|
339
|
+
}
|
|
340
|
+
const fraction = value.split(".")[1] ?? "";
|
|
341
|
+
if (fraction.length > decimals)
|
|
342
|
+
return { kind: "invalid", reason: "precision" };
|
|
343
|
+
if (amount === 0n) return { kind: "skip" };
|
|
344
|
+
if (amount > maxAmount) return { kind: "invalid", reason: "above-cap" };
|
|
345
|
+
return { kind: "settle", amount: amount.toString() };
|
|
346
|
+
}
|
|
347
|
+
var INVALID_SETTLE_AMOUNT_RESPONSE = {
|
|
348
|
+
error: "INVALID_SETTLE_AMOUNT",
|
|
349
|
+
message: `The handler set an invalid ${SETTLE_AMOUNT_HEADER} header \u2014 expected a decimal amount in token units, formatted like the route price and within the token's precision, at most the route price.`
|
|
350
|
+
};
|
|
288
351
|
function resolveAcceptedTokens(config) {
|
|
289
352
|
if (config.acceptedTokens?.length) {
|
|
290
353
|
return config.acceptedTokens;
|
|
@@ -309,12 +372,12 @@ function buildPaymentRequired(url, config, resolvedTokens) {
|
|
|
309
372
|
const accepts = resolvedTokens.map((asset) => {
|
|
310
373
|
const decimals = resolveDecimals(config, asset);
|
|
311
374
|
return {
|
|
312
|
-
scheme: "exact",
|
|
375
|
+
scheme: config.scheme ?? "exact",
|
|
313
376
|
network,
|
|
314
377
|
asset,
|
|
315
378
|
amount: parsePrice(config.price, decimals).toString(),
|
|
316
379
|
payTo: config.payTo,
|
|
317
|
-
maxTimeoutSeconds:
|
|
380
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
|
|
318
381
|
extra: { decimals }
|
|
319
382
|
};
|
|
320
383
|
});
|
|
@@ -360,11 +423,17 @@ function resolveDeliveredAsset(header, config, resolvedTokens) {
|
|
|
360
423
|
}
|
|
361
424
|
function buildFacilitatorRequirements(config, asset) {
|
|
362
425
|
const decimals = resolveDecimals(config, asset);
|
|
426
|
+
const scheme = config.scheme ?? "exact";
|
|
363
427
|
return {
|
|
364
428
|
maxAmountRequired: parsePrice(config.price, decimals).toString(),
|
|
365
429
|
asset,
|
|
366
430
|
payTo: config.payTo,
|
|
367
|
-
network: config.network
|
|
431
|
+
network: config.network,
|
|
432
|
+
// Only metered routes name their scheme — exact requirements are unchanged.
|
|
433
|
+
...scheme === "upto" && {
|
|
434
|
+
scheme,
|
|
435
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS
|
|
436
|
+
}
|
|
368
437
|
};
|
|
369
438
|
}
|
|
370
439
|
function paymentRequiredResponse(url, config, resolvedTokens, error) {
|
|
@@ -407,10 +476,12 @@ function createPaymentGate(config) {
|
|
|
407
476
|
)
|
|
408
477
|
};
|
|
409
478
|
}
|
|
410
|
-
const
|
|
479
|
+
const asset = resolveDeliveredAsset(
|
|
480
|
+
paymentHeader,
|
|
411
481
|
routeConfig,
|
|
412
|
-
|
|
482
|
+
resolvedTokens
|
|
413
483
|
);
|
|
484
|
+
const requirements = buildFacilitatorRequirements(routeConfig, asset);
|
|
414
485
|
try {
|
|
415
486
|
await client.verify(paymentHeader, requirements);
|
|
416
487
|
} catch (e) {
|
|
@@ -427,7 +498,12 @@ function createPaymentGate(config) {
|
|
|
427
498
|
}
|
|
428
499
|
return {
|
|
429
500
|
type: "verified",
|
|
430
|
-
|
|
501
|
+
scheme: routeConfig.scheme ?? "exact",
|
|
502
|
+
maxAmount: BigInt(requirements.maxAmountRequired),
|
|
503
|
+
decimals: resolveDecimals(routeConfig, asset),
|
|
504
|
+
handlerTimeoutMs: handlerTimeoutFor(routeConfig),
|
|
505
|
+
// Exact routes keep the historical two-argument client call.
|
|
506
|
+
settle: (options) => options ? client.settle(paymentHeader, requirements, options) : client.settle(paymentHeader, requirements)
|
|
431
507
|
};
|
|
432
508
|
};
|
|
433
509
|
}
|
|
@@ -439,14 +515,42 @@ function createPaymentMiddleware(config) {
|
|
|
439
515
|
if (gateResult.type === "error") return gateResult.result;
|
|
440
516
|
let handlerResult;
|
|
441
517
|
try {
|
|
442
|
-
handlerResult = await adapter.runHandler(
|
|
518
|
+
handlerResult = await adapter.runHandler({
|
|
519
|
+
timeoutMs: gateResult.handlerTimeoutMs
|
|
520
|
+
});
|
|
443
521
|
} catch {
|
|
444
522
|
return { pass: true, handlerRan: true };
|
|
445
523
|
}
|
|
524
|
+
let raw;
|
|
525
|
+
if (gateResult.scheme === "upto") {
|
|
526
|
+
raw = handlerResult.getResponseHeader?.(SETTLE_AMOUNT_HEADER);
|
|
527
|
+
handlerResult.removeResponseHeader?.(SETTLE_AMOUNT_HEADER);
|
|
528
|
+
}
|
|
446
529
|
if (handlerResult.status >= 200 && handlerResult.status < 300) {
|
|
530
|
+
let settleOptions;
|
|
531
|
+
if (gateResult.scheme === "upto") {
|
|
532
|
+
const resolved = resolveSettleAmount(
|
|
533
|
+
raw,
|
|
534
|
+
gateResult.maxAmount,
|
|
535
|
+
gateResult.decimals
|
|
536
|
+
);
|
|
537
|
+
if (resolved.kind === "invalid") {
|
|
538
|
+
handlerResult.discardResponse();
|
|
539
|
+
return {
|
|
540
|
+
status: 402,
|
|
541
|
+
headers: { "Content-Type": "application/json" },
|
|
542
|
+
body: INVALID_SETTLE_AMOUNT_RESPONSE
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (resolved.kind === "skip") {
|
|
546
|
+
handlerResult.sendResponse();
|
|
547
|
+
return { pass: true, handlerRan: true };
|
|
548
|
+
}
|
|
549
|
+
settleOptions = { amount: resolved.amount };
|
|
550
|
+
}
|
|
447
551
|
let settled = false;
|
|
448
552
|
try {
|
|
449
|
-
const settleResponse = await gateResult.settle();
|
|
553
|
+
const settleResponse = settleOptions ? await gateResult.settle(settleOptions) : await gateResult.settle();
|
|
450
554
|
if (settleResponse.success) {
|
|
451
555
|
handlerResult.setResponseHeader(
|
|
452
556
|
"payment-response",
|
|
@@ -478,7 +582,10 @@ function createPaymentMiddleware(config) {
|
|
|
478
582
|
}
|
|
479
583
|
|
|
480
584
|
exports.FacilitatorClient = FacilitatorClient;
|
|
585
|
+
exports.HANDLER_TIMEOUT = HANDLER_TIMEOUT;
|
|
586
|
+
exports.INVALID_SETTLE_AMOUNT_RESPONSE = INVALID_SETTLE_AMOUNT_RESPONSE;
|
|
481
587
|
exports.PARALLEL_TOKENS = PARALLEL_TOKENS;
|
|
588
|
+
exports.SETTLE_AMOUNT_HEADER = SETTLE_AMOUNT_HEADER;
|
|
482
589
|
exports.X402ConfigError = X402ConfigError;
|
|
483
590
|
exports.X402RuntimeError = X402RuntimeError;
|
|
484
591
|
exports.X402_ERROR_CODES = X402_ERROR_CODES;
|
|
@@ -487,5 +594,7 @@ exports.createPaymentMiddleware = createPaymentMiddleware;
|
|
|
487
594
|
exports.encodeBase64 = encodeBase64;
|
|
488
595
|
exports.getDefaultAcceptedTokens = getDefaultAcceptedTokens;
|
|
489
596
|
exports.parsePrice = parsePrice;
|
|
597
|
+
exports.raceTimeout = raceTimeout;
|
|
598
|
+
exports.resolveSettleAmount = resolveSettleAmount;
|
|
490
599
|
exports.toEip155Network = toEip155Network;
|
|
491
600
|
exports.validateAddress = validateAddress;
|
|
@@ -80,13 +80,19 @@ var facilitatorConfigSchema = z.object({
|
|
|
80
80
|
url: z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
|
|
81
81
|
apiKey: z.string().optional()
|
|
82
82
|
});
|
|
83
|
+
var UPTO_MAX_TIMEOUT_SECONDS = 3600;
|
|
83
84
|
var routeConfigSchema = z.object({
|
|
84
85
|
price: priceSchema,
|
|
85
86
|
network: z.string().min(1, "Network is required"),
|
|
86
87
|
payTo: ethereumAddressSchema,
|
|
87
88
|
acceptedTokens: z.array(ethereumAddressSchema).optional(),
|
|
88
|
-
description: z.string().optional()
|
|
89
|
-
|
|
89
|
+
description: z.string().optional(),
|
|
90
|
+
scheme: z.enum(["exact", "upto"]).optional(),
|
|
91
|
+
maxTimeoutSeconds: z.number().int().positive().optional()
|
|
92
|
+
}).refine(
|
|
93
|
+
(route) => route.scheme !== "upto" || (route.maxTimeoutSeconds ?? 300) <= UPTO_MAX_TIMEOUT_SECONDS,
|
|
94
|
+
`maxTimeoutSeconds must be at most ${UPTO_MAX_TIMEOUT_SECONDS} on an upto route`
|
|
95
|
+
);
|
|
90
96
|
var paymentMiddlewareConfigSchema = z.object({
|
|
91
97
|
facilitator: facilitatorConfigSchema,
|
|
92
98
|
routes: z.record(z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
|
|
@@ -98,7 +104,8 @@ var facilitatorResponseSchema = z.discriminatedUnion("success", [
|
|
|
98
104
|
route: z.string(),
|
|
99
105
|
chain: z.string(),
|
|
100
106
|
gasSponsored: z.boolean(),
|
|
101
|
-
networkId: z.string()
|
|
107
|
+
networkId: z.string(),
|
|
108
|
+
amountSettled: z.string().optional()
|
|
102
109
|
}),
|
|
103
110
|
z.object({
|
|
104
111
|
success: z.literal(false),
|
|
@@ -115,7 +122,9 @@ var verifyResponseSchema = z.object({
|
|
|
115
122
|
var settleSuccessSchema = z.object({
|
|
116
123
|
txHash: txHashSchema,
|
|
117
124
|
route: z.string(),
|
|
118
|
-
gasSponsored: z.boolean()
|
|
125
|
+
gasSponsored: z.boolean(),
|
|
126
|
+
// Metered routes: the amount actually charged (smallest unit).
|
|
127
|
+
amountSettled: z.string().optional()
|
|
119
128
|
});
|
|
120
129
|
var settleErrorSchema = z.object({
|
|
121
130
|
error: z.string(),
|
|
@@ -167,14 +176,21 @@ var FacilitatorClient = class {
|
|
|
167
176
|
throw new X402RuntimeError(X402_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
|
|
168
177
|
}
|
|
169
178
|
}
|
|
170
|
-
async settle(paymentHeader, paymentRequirements) {
|
|
179
|
+
async settle(paymentHeader, paymentRequirements, options) {
|
|
171
180
|
const paymentPayload = decodePaymentHeader(paymentHeader);
|
|
172
181
|
let json;
|
|
173
182
|
try {
|
|
174
183
|
const response = await fetch(`${this.config.url}/x402/settle`, {
|
|
175
184
|
method: "POST",
|
|
176
185
|
headers: this.buildHeaders(),
|
|
177
|
-
|
|
186
|
+
// settleAmount only travels for metered routes — exact bodies are unchanged.
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
paymentPayload,
|
|
189
|
+
paymentRequirements,
|
|
190
|
+
...options?.amount !== void 0 && {
|
|
191
|
+
settleAmount: options.amount
|
|
192
|
+
}
|
|
193
|
+
}),
|
|
178
194
|
signal: AbortSignal.timeout(SETTLE_TIMEOUT_MS)
|
|
179
195
|
});
|
|
180
196
|
if (!response.ok) {
|
|
@@ -197,7 +213,10 @@ var FacilitatorClient = class {
|
|
|
197
213
|
route: parsed.route,
|
|
198
214
|
chain: paymentRequirements.network,
|
|
199
215
|
gasSponsored: parsed.gasSponsored,
|
|
200
|
-
networkId: toEip155Network(paymentRequirements.network)
|
|
216
|
+
networkId: toEip155Network(paymentRequirements.network),
|
|
217
|
+
...parsed.amountSettled !== void 0 && {
|
|
218
|
+
amountSettled: parsed.amountSettled
|
|
219
|
+
}
|
|
201
220
|
};
|
|
202
221
|
}
|
|
203
222
|
return {
|
|
@@ -242,6 +261,22 @@ var FacilitatorClient = class {
|
|
|
242
261
|
return headers;
|
|
243
262
|
}
|
|
244
263
|
};
|
|
264
|
+
|
|
265
|
+
// src/timeout.ts
|
|
266
|
+
var HANDLER_TIMEOUT_MS = 3e4;
|
|
267
|
+
var HANDLER_TIMEOUT = /* @__PURE__ */ Symbol("x402:handler-timeout");
|
|
268
|
+
async function raceTimeout(promise, ms = HANDLER_TIMEOUT_MS) {
|
|
269
|
+
let timer;
|
|
270
|
+
const timeout = new Promise((resolve) => {
|
|
271
|
+
timer = setTimeout(() => resolve(HANDLER_TIMEOUT), ms);
|
|
272
|
+
timer.unref?.();
|
|
273
|
+
});
|
|
274
|
+
try {
|
|
275
|
+
return await Promise.race([promise, timeout]);
|
|
276
|
+
} finally {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
245
280
|
var PARALLEL_TOKENS = {
|
|
246
281
|
ethereum: {
|
|
247
282
|
usdp: "0x9B3a8f7CEC208e247d97dEE13313690977e24459",
|
|
@@ -283,6 +318,34 @@ function getTokenDecimals(network, address) {
|
|
|
283
318
|
}
|
|
284
319
|
|
|
285
320
|
// src/middleware.ts
|
|
321
|
+
var SETTLE_AMOUNT_HEADER = "payment-settle-amount";
|
|
322
|
+
var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
|
|
323
|
+
function handlerTimeoutFor(config) {
|
|
324
|
+
if ((config.scheme ?? "exact") !== "upto") return HANDLER_TIMEOUT_MS;
|
|
325
|
+
const seconds = config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS;
|
|
326
|
+
return Math.max(HANDLER_TIMEOUT_MS, (seconds - 60) * 1e3);
|
|
327
|
+
}
|
|
328
|
+
function resolveSettleAmount(raw, maxAmount, decimals) {
|
|
329
|
+
if (raw === void 0)
|
|
330
|
+
return { kind: "settle", amount: maxAmount.toString() };
|
|
331
|
+
const value = raw.trim();
|
|
332
|
+
let amount;
|
|
333
|
+
try {
|
|
334
|
+
amount = parsePrice(value, decimals);
|
|
335
|
+
} catch {
|
|
336
|
+
return { kind: "invalid", reason: "malformed" };
|
|
337
|
+
}
|
|
338
|
+
const fraction = value.split(".")[1] ?? "";
|
|
339
|
+
if (fraction.length > decimals)
|
|
340
|
+
return { kind: "invalid", reason: "precision" };
|
|
341
|
+
if (amount === 0n) return { kind: "skip" };
|
|
342
|
+
if (amount > maxAmount) return { kind: "invalid", reason: "above-cap" };
|
|
343
|
+
return { kind: "settle", amount: amount.toString() };
|
|
344
|
+
}
|
|
345
|
+
var INVALID_SETTLE_AMOUNT_RESPONSE = {
|
|
346
|
+
error: "INVALID_SETTLE_AMOUNT",
|
|
347
|
+
message: `The handler set an invalid ${SETTLE_AMOUNT_HEADER} header \u2014 expected a decimal amount in token units, formatted like the route price and within the token's precision, at most the route price.`
|
|
348
|
+
};
|
|
286
349
|
function resolveAcceptedTokens(config) {
|
|
287
350
|
if (config.acceptedTokens?.length) {
|
|
288
351
|
return config.acceptedTokens;
|
|
@@ -307,12 +370,12 @@ function buildPaymentRequired(url, config, resolvedTokens) {
|
|
|
307
370
|
const accepts = resolvedTokens.map((asset) => {
|
|
308
371
|
const decimals = resolveDecimals(config, asset);
|
|
309
372
|
return {
|
|
310
|
-
scheme: "exact",
|
|
373
|
+
scheme: config.scheme ?? "exact",
|
|
311
374
|
network,
|
|
312
375
|
asset,
|
|
313
376
|
amount: parsePrice(config.price, decimals).toString(),
|
|
314
377
|
payTo: config.payTo,
|
|
315
|
-
maxTimeoutSeconds:
|
|
378
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
|
|
316
379
|
extra: { decimals }
|
|
317
380
|
};
|
|
318
381
|
});
|
|
@@ -358,11 +421,17 @@ function resolveDeliveredAsset(header, config, resolvedTokens) {
|
|
|
358
421
|
}
|
|
359
422
|
function buildFacilitatorRequirements(config, asset) {
|
|
360
423
|
const decimals = resolveDecimals(config, asset);
|
|
424
|
+
const scheme = config.scheme ?? "exact";
|
|
361
425
|
return {
|
|
362
426
|
maxAmountRequired: parsePrice(config.price, decimals).toString(),
|
|
363
427
|
asset,
|
|
364
428
|
payTo: config.payTo,
|
|
365
|
-
network: config.network
|
|
429
|
+
network: config.network,
|
|
430
|
+
// Only metered routes name their scheme — exact requirements are unchanged.
|
|
431
|
+
...scheme === "upto" && {
|
|
432
|
+
scheme,
|
|
433
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS
|
|
434
|
+
}
|
|
366
435
|
};
|
|
367
436
|
}
|
|
368
437
|
function paymentRequiredResponse(url, config, resolvedTokens, error) {
|
|
@@ -405,10 +474,12 @@ function createPaymentGate(config) {
|
|
|
405
474
|
)
|
|
406
475
|
};
|
|
407
476
|
}
|
|
408
|
-
const
|
|
477
|
+
const asset = resolveDeliveredAsset(
|
|
478
|
+
paymentHeader,
|
|
409
479
|
routeConfig,
|
|
410
|
-
|
|
480
|
+
resolvedTokens
|
|
411
481
|
);
|
|
482
|
+
const requirements = buildFacilitatorRequirements(routeConfig, asset);
|
|
412
483
|
try {
|
|
413
484
|
await client.verify(paymentHeader, requirements);
|
|
414
485
|
} catch (e) {
|
|
@@ -425,7 +496,12 @@ function createPaymentGate(config) {
|
|
|
425
496
|
}
|
|
426
497
|
return {
|
|
427
498
|
type: "verified",
|
|
428
|
-
|
|
499
|
+
scheme: routeConfig.scheme ?? "exact",
|
|
500
|
+
maxAmount: BigInt(requirements.maxAmountRequired),
|
|
501
|
+
decimals: resolveDecimals(routeConfig, asset),
|
|
502
|
+
handlerTimeoutMs: handlerTimeoutFor(routeConfig),
|
|
503
|
+
// Exact routes keep the historical two-argument client call.
|
|
504
|
+
settle: (options) => options ? client.settle(paymentHeader, requirements, options) : client.settle(paymentHeader, requirements)
|
|
429
505
|
};
|
|
430
506
|
};
|
|
431
507
|
}
|
|
@@ -437,14 +513,42 @@ function createPaymentMiddleware(config) {
|
|
|
437
513
|
if (gateResult.type === "error") return gateResult.result;
|
|
438
514
|
let handlerResult;
|
|
439
515
|
try {
|
|
440
|
-
handlerResult = await adapter.runHandler(
|
|
516
|
+
handlerResult = await adapter.runHandler({
|
|
517
|
+
timeoutMs: gateResult.handlerTimeoutMs
|
|
518
|
+
});
|
|
441
519
|
} catch {
|
|
442
520
|
return { pass: true, handlerRan: true };
|
|
443
521
|
}
|
|
522
|
+
let raw;
|
|
523
|
+
if (gateResult.scheme === "upto") {
|
|
524
|
+
raw = handlerResult.getResponseHeader?.(SETTLE_AMOUNT_HEADER);
|
|
525
|
+
handlerResult.removeResponseHeader?.(SETTLE_AMOUNT_HEADER);
|
|
526
|
+
}
|
|
444
527
|
if (handlerResult.status >= 200 && handlerResult.status < 300) {
|
|
528
|
+
let settleOptions;
|
|
529
|
+
if (gateResult.scheme === "upto") {
|
|
530
|
+
const resolved = resolveSettleAmount(
|
|
531
|
+
raw,
|
|
532
|
+
gateResult.maxAmount,
|
|
533
|
+
gateResult.decimals
|
|
534
|
+
);
|
|
535
|
+
if (resolved.kind === "invalid") {
|
|
536
|
+
handlerResult.discardResponse();
|
|
537
|
+
return {
|
|
538
|
+
status: 402,
|
|
539
|
+
headers: { "Content-Type": "application/json" },
|
|
540
|
+
body: INVALID_SETTLE_AMOUNT_RESPONSE
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (resolved.kind === "skip") {
|
|
544
|
+
handlerResult.sendResponse();
|
|
545
|
+
return { pass: true, handlerRan: true };
|
|
546
|
+
}
|
|
547
|
+
settleOptions = { amount: resolved.amount };
|
|
548
|
+
}
|
|
445
549
|
let settled = false;
|
|
446
550
|
try {
|
|
447
|
-
const settleResponse = await gateResult.settle();
|
|
551
|
+
const settleResponse = settleOptions ? await gateResult.settle(settleOptions) : await gateResult.settle();
|
|
448
552
|
if (settleResponse.success) {
|
|
449
553
|
handlerResult.setResponseHeader(
|
|
450
554
|
"payment-response",
|
|
@@ -475,4 +579,4 @@ function createPaymentMiddleware(config) {
|
|
|
475
579
|
};
|
|
476
580
|
}
|
|
477
581
|
|
|
478
|
-
export { FacilitatorClient, PARALLEL_TOKENS, X402ConfigError, X402RuntimeError, X402_ERROR_CODES, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, parsePrice, toEip155Network, validateAddress };
|
|
582
|
+
export { FacilitatorClient, HANDLER_TIMEOUT, INVALID_SETTLE_AMOUNT_RESPONSE, PARALLEL_TOKENS, SETTLE_AMOUNT_HEADER, X402ConfigError, X402RuntimeError, X402_ERROR_CODES, createPaymentGate, createPaymentMiddleware, encodeBase64, getDefaultAcceptedTokens, parsePrice, raceTimeout, resolveSettleAmount, toEip155Network, validateAddress };
|
package/dist/express.cjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var chunkKQPIZ6SZ_cjs = require('./chunk-KQPIZ6SZ.cjs');
|
|
3
|
+
var chunkK6QETMQA_cjs = require('./chunk-K6QETMQA.cjs');
|
|
5
4
|
|
|
6
5
|
// src/express/adapter.ts
|
|
6
|
+
function headerValue(value) {
|
|
7
|
+
if (value === void 0) return void 0;
|
|
8
|
+
return Array.isArray(value) ? value[0] : String(value);
|
|
9
|
+
}
|
|
7
10
|
var TIMEOUT_BODY = JSON.stringify({
|
|
8
11
|
error: "HANDLER_TIMEOUT",
|
|
9
12
|
message: "The resource handler did not respond in time."
|
|
@@ -30,7 +33,7 @@ var ExpressAdapter = class {
|
|
|
30
33
|
getUrl() {
|
|
31
34
|
return `${this.req.protocol}://${this.req.headers.host}${this.req.originalUrl}`;
|
|
32
35
|
}
|
|
33
|
-
async runHandler() {
|
|
36
|
+
async runHandler(options) {
|
|
34
37
|
const res = this.res;
|
|
35
38
|
const next = this.next;
|
|
36
39
|
const originalWriteHead = res.writeHead.bind(res);
|
|
@@ -74,14 +77,17 @@ var ExpressAdapter = class {
|
|
|
74
77
|
return originalEnd(...args);
|
|
75
78
|
});
|
|
76
79
|
next();
|
|
77
|
-
const outcome = await
|
|
78
|
-
if (outcome ===
|
|
80
|
+
const outcome = await chunkK6QETMQA_cjs.raceTimeout(endPromise, options?.timeoutMs);
|
|
81
|
+
if (outcome === chunkK6QETMQA_cjs.HANDLER_TIMEOUT) {
|
|
79
82
|
discarded = true;
|
|
80
83
|
return {
|
|
81
84
|
status: 504,
|
|
82
85
|
setResponseHeader: (name, value) => {
|
|
83
86
|
if (!res.headersSent) res.setHeader(name, value);
|
|
84
87
|
},
|
|
88
|
+
getResponseHeader: () => void 0,
|
|
89
|
+
removeResponseHeader: () => {
|
|
90
|
+
},
|
|
85
91
|
sendResponse: () => {
|
|
86
92
|
if (res.headersSent) return;
|
|
87
93
|
originalWriteHead(504, { "Content-Type": "application/json" });
|
|
@@ -94,6 +100,8 @@ var ExpressAdapter = class {
|
|
|
94
100
|
return {
|
|
95
101
|
status: res.statusCode,
|
|
96
102
|
setResponseHeader: (name, value) => res.setHeader(name, value),
|
|
103
|
+
getResponseHeader: (name) => headerValue(res.getHeader(name)),
|
|
104
|
+
removeResponseHeader: (name) => res.removeHeader(name),
|
|
97
105
|
sendResponse: () => {
|
|
98
106
|
settled = true;
|
|
99
107
|
restore();
|
|
@@ -113,7 +121,7 @@ var ExpressAdapter = class {
|
|
|
113
121
|
|
|
114
122
|
// src/express/index.ts
|
|
115
123
|
function paymentMiddleware(config) {
|
|
116
|
-
const middleware =
|
|
124
|
+
const middleware = chunkK6QETMQA_cjs.createPaymentMiddleware(config);
|
|
117
125
|
return async (req, res, next) => {
|
|
118
126
|
const result = await middleware(new ExpressAdapter(req, res, next));
|
|
119
127
|
if ("pass" in result) {
|