@lunora/x402 1.0.0-alpha.3 → 1.0.0-alpha.4

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.
@@ -1,5 +1,5 @@
1
- import { F as FacilitatorConfig, c as X402ChargeConfig, f as X402Price } from "../packem_shared/config.d-D8gKQLAq.mjs";
2
- export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, type a as EvmAddress, type P as PaymentEventRow, type d as X402Network, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2, x as toPaymentEventRow, y as toReceipt } from "../packem_shared/config.d-D8gKQLAq.mjs";
1
+ import { F as FacilitatorConfig, c as X402ChargeConfig, f as X402Price } from "../packem_shared/config.d-5Nqi5iox.mjs";
2
+ export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, type a as EvmAddress, type P as PaymentEventRow, type d as X402Network, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2, x as toPaymentEventRow, y as toReceipt } from "../packem_shared/config.d-5Nqi5iox.mjs";
3
3
  import { HTTPFacilitatorClient } from '@x402/core/server';
4
4
  import { RouteConfig } from '@x402/core/http';
5
5
  import '@x402/evm';
@@ -34,12 +34,24 @@ declare const withX402: <Context>(config: X402ChargeConfig, handler: HttpActionH
34
34
  */
35
35
  type ChargeHandler = () => Promise<Response> | Response;
36
36
  /**
37
+ * Per-request platform seams `handle` can use, beyond the request/handler pair.
38
+ * @experimental
39
+ */
40
+ interface ChargeHandlerDeps {
41
+ /**
42
+ * Keep background work (the receipt sink) alive past the response — the
43
+ * request's `ctx.waitUntil`. Absent on paths with no platform execution
44
+ * context reaching the middleware (e.g. today's HTTP-action rail).
45
+ */
46
+ readonly waitUntil?: (promise: Promise<unknown>) => void;
47
+ }
48
+ /**
37
49
  * A prepared, initialised paywall. Build once (it fetches facilitator support), reuse per request.
38
50
  * @experimental
39
51
  */
40
52
  interface ChargeMiddleware {
41
53
  /** Gate `request`: challenge / verify / settle around `runHandler`. */
42
- handle: (request: Request, runHandler: ChargeHandler) => Promise<Response>;
54
+ handle: (request: Request, runHandler: ChargeHandler, deps?: ChargeHandlerDeps) => Promise<Response>;
43
55
  }
44
56
  /**
45
57
  * Route metadata a caller can layer onto the generated catch-all route. The
@@ -51,13 +63,39 @@ interface ChargeMiddleware {
51
63
  */
52
64
  type ChargeRouteOverrides = Pick<RouteConfig, "description" | "resource">;
53
65
  /**
66
+ * Behaviour knobs for {@link createChargeMiddleware} beyond route metadata.
67
+ * @experimental
68
+ */
69
+ interface ChargeMiddlewareOptions {
70
+ /**
71
+ * Settle the verified payment **before** dispatching `runHandler`, instead
72
+ * of after. Use this for a mutation/procedure gate: a settlement failure
73
+ * then means the handler never runs at all, so a paid mutation's writes can
74
+ * never be committed without payment (the free-execution gap X402-04
75
+ * closes). Once settlement succeeds the payment is final (on-chain) — a
76
+ * handler failure after that point is a normal application error, not a
77
+ * payment to unwind: there is nothing left to cancel, so it is not caught
78
+ * here and simply propagates.
79
+ *
80
+ * Default `false` (settle-after, the historical behaviour): the handler's
81
+ * response is passed to settlement as transport context (`responseHeaders`
82
+ * — read by some schemes for settlement overrides), and a handler throw
83
+ * still releases the verified-but-unsettled payment via
84
+ * `cancellationDispatcher.cancel`. Because settlement can still fail after
85
+ * the handler already ran on this path, `.x402()` handlers gated this way
86
+ * MUST be idempotent or compensatable — see the `@lunora/x402` charge docs.
87
+ */
88
+ readonly settleBeforeHandler?: boolean;
89
+ }
90
+ /**
54
91
  * Build and initialise a {@link ChargeMiddleware} for `config`. Fetches
55
92
  * facilitator support once (via `initialize()`), so call this once per config
56
93
  * and reuse the result across requests. `routeOverrides` layers extra route
57
- * metadata (e.g. `resource`) onto the generated catch-all route.
94
+ * metadata (e.g. `resource`) onto the generated catch-all route; `options`
95
+ * controls settlement ordering (see {@link ChargeMiddlewareOptions}).
58
96
  * @experimental
59
97
  */
60
- declare const createChargeMiddleware: (config: X402ChargeConfig, routeOverrides?: ChargeRouteOverrides) => Promise<ChargeMiddleware>;
98
+ declare const createChargeMiddleware: (config: X402ChargeConfig, routeOverrides?: ChargeRouteOverrides, options?: ChargeMiddlewareOptions) => Promise<ChargeMiddleware>;
61
99
  /**
62
100
  * Charge config for the procedure gate: the worker-level settlement vocabulary
63
101
  * (network, recipient, facilitator) minus `price` — price is per-procedure and
@@ -79,17 +117,23 @@ interface X402ProcedureSpec {
79
117
  * Gate one paid RPC. Returns a real `402` + `PAYMENT-REQUIRED` challenge when the
80
118
  * request is unpaid, or the dispatched response (with `X-PAYMENT-RESPONSE`
81
119
  * attached) once the client's `X-PAYMENT` is verified and settled. `dispatch`
82
- * runs the actual shard forward — it is only invoked after payment is verified.
120
+ * runs the actual shard forward — settlement happens **before** `dispatch` is
121
+ * invoked (settle-first), so a settlement failure means the shard forward
122
+ * (the mutation's commit) never runs at all — no committed-but-unpaid write is
123
+ * possible. `deps.waitUntil`, when supplied (the request's `ctx.waitUntil`),
124
+ * keeps the opt-in receipt sink alive past the response.
83
125
  * @experimental
84
126
  */
85
- type X402ProcedureChargeGate = (request: Request, spec: X402ProcedureSpec, dispatch: () => Promise<Response>) => Promise<Response>;
127
+ type X402ProcedureChargeGate = (request: Request, spec: X402ProcedureSpec, dispatch: () => Promise<Response>, deps?: ChargeHandlerDeps) => Promise<Response>;
86
128
  /**
87
129
  * Build the injectable procedure charge gate for `config`. One initialised
88
130
  * {@link ChargeMiddleware} is memoised per `functionPath` (each bakes that
89
131
  * function's price + `resource`), since `createChargeMiddleware` fetches
90
132
  * facilitator support on first use. A failed init is not cached, so a transient
91
- * facilitator outage retries on the next request.
133
+ * facilitator outage retries on the next request. Settlement runs before
134
+ * `dispatch` (`settleBeforeHandler: true`) since `dispatch` commits the
135
+ * procedure's real mutation — see `createChargeMiddleware`'s `ChargeMiddlewareOptions`.
92
136
  * @experimental
93
137
  */
94
138
  declare const createProcedureChargeGate: (config: X402ProcedureChargeConfig) => X402ProcedureChargeGate;
95
- export { type ChargeHandler, type ChargeMiddleware, type ChargeRouteOverrides, type FacilitatorConfig, type HttpActionHandler, type X402ChargeConfig, type X402Price, type X402ProcedureChargeConfig, type X402ProcedureChargeGate, type X402ProcedureSpec, createChargeMiddleware, createFacilitatorClient, createProcedureChargeGate, withX402 };
139
+ export { type ChargeHandler, type ChargeHandlerDeps, type ChargeMiddleware, type ChargeMiddlewareOptions, type ChargeRouteOverrides, type FacilitatorConfig, type HttpActionHandler, type X402ChargeConfig, type X402Price, type X402ProcedureChargeConfig, type X402ProcedureChargeGate, type X402ProcedureSpec, createChargeMiddleware, createFacilitatorClient, createProcedureChargeGate, withX402 };
@@ -1,5 +1,5 @@
1
- import { F as FacilitatorConfig, c as X402ChargeConfig, f as X402Price } from "../packem_shared/config.d-D8gKQLAq.js";
2
- export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, type a as EvmAddress, type P as PaymentEventRow, type d as X402Network, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2, x as toPaymentEventRow, y as toReceipt } from "../packem_shared/config.d-D8gKQLAq.js";
1
+ import { F as FacilitatorConfig, c as X402ChargeConfig, f as X402Price } from "../packem_shared/config.d-5Nqi5iox.js";
2
+ export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, type a as EvmAddress, type P as PaymentEventRow, type d as X402Network, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2, x as toPaymentEventRow, y as toReceipt } from "../packem_shared/config.d-5Nqi5iox.js";
3
3
  import { HTTPFacilitatorClient } from '@x402/core/server';
4
4
  import { RouteConfig } from '@x402/core/http';
5
5
  import '@x402/evm';
@@ -34,12 +34,24 @@ declare const withX402: <Context>(config: X402ChargeConfig, handler: HttpActionH
34
34
  */
35
35
  type ChargeHandler = () => Promise<Response> | Response;
36
36
  /**
37
+ * Per-request platform seams `handle` can use, beyond the request/handler pair.
38
+ * @experimental
39
+ */
40
+ interface ChargeHandlerDeps {
41
+ /**
42
+ * Keep background work (the receipt sink) alive past the response — the
43
+ * request's `ctx.waitUntil`. Absent on paths with no platform execution
44
+ * context reaching the middleware (e.g. today's HTTP-action rail).
45
+ */
46
+ readonly waitUntil?: (promise: Promise<unknown>) => void;
47
+ }
48
+ /**
37
49
  * A prepared, initialised paywall. Build once (it fetches facilitator support), reuse per request.
38
50
  * @experimental
39
51
  */
40
52
  interface ChargeMiddleware {
41
53
  /** Gate `request`: challenge / verify / settle around `runHandler`. */
42
- handle: (request: Request, runHandler: ChargeHandler) => Promise<Response>;
54
+ handle: (request: Request, runHandler: ChargeHandler, deps?: ChargeHandlerDeps) => Promise<Response>;
43
55
  }
44
56
  /**
45
57
  * Route metadata a caller can layer onto the generated catch-all route. The
@@ -51,13 +63,39 @@ interface ChargeMiddleware {
51
63
  */
52
64
  type ChargeRouteOverrides = Pick<RouteConfig, "description" | "resource">;
53
65
  /**
66
+ * Behaviour knobs for {@link createChargeMiddleware} beyond route metadata.
67
+ * @experimental
68
+ */
69
+ interface ChargeMiddlewareOptions {
70
+ /**
71
+ * Settle the verified payment **before** dispatching `runHandler`, instead
72
+ * of after. Use this for a mutation/procedure gate: a settlement failure
73
+ * then means the handler never runs at all, so a paid mutation's writes can
74
+ * never be committed without payment (the free-execution gap X402-04
75
+ * closes). Once settlement succeeds the payment is final (on-chain) — a
76
+ * handler failure after that point is a normal application error, not a
77
+ * payment to unwind: there is nothing left to cancel, so it is not caught
78
+ * here and simply propagates.
79
+ *
80
+ * Default `false` (settle-after, the historical behaviour): the handler's
81
+ * response is passed to settlement as transport context (`responseHeaders`
82
+ * — read by some schemes for settlement overrides), and a handler throw
83
+ * still releases the verified-but-unsettled payment via
84
+ * `cancellationDispatcher.cancel`. Because settlement can still fail after
85
+ * the handler already ran on this path, `.x402()` handlers gated this way
86
+ * MUST be idempotent or compensatable — see the `@lunora/x402` charge docs.
87
+ */
88
+ readonly settleBeforeHandler?: boolean;
89
+ }
90
+ /**
54
91
  * Build and initialise a {@link ChargeMiddleware} for `config`. Fetches
55
92
  * facilitator support once (via `initialize()`), so call this once per config
56
93
  * and reuse the result across requests. `routeOverrides` layers extra route
57
- * metadata (e.g. `resource`) onto the generated catch-all route.
94
+ * metadata (e.g. `resource`) onto the generated catch-all route; `options`
95
+ * controls settlement ordering (see {@link ChargeMiddlewareOptions}).
58
96
  * @experimental
59
97
  */
60
- declare const createChargeMiddleware: (config: X402ChargeConfig, routeOverrides?: ChargeRouteOverrides) => Promise<ChargeMiddleware>;
98
+ declare const createChargeMiddleware: (config: X402ChargeConfig, routeOverrides?: ChargeRouteOverrides, options?: ChargeMiddlewareOptions) => Promise<ChargeMiddleware>;
61
99
  /**
62
100
  * Charge config for the procedure gate: the worker-level settlement vocabulary
63
101
  * (network, recipient, facilitator) minus `price` — price is per-procedure and
@@ -79,17 +117,23 @@ interface X402ProcedureSpec {
79
117
  * Gate one paid RPC. Returns a real `402` + `PAYMENT-REQUIRED` challenge when the
80
118
  * request is unpaid, or the dispatched response (with `X-PAYMENT-RESPONSE`
81
119
  * attached) once the client's `X-PAYMENT` is verified and settled. `dispatch`
82
- * runs the actual shard forward — it is only invoked after payment is verified.
120
+ * runs the actual shard forward — settlement happens **before** `dispatch` is
121
+ * invoked (settle-first), so a settlement failure means the shard forward
122
+ * (the mutation's commit) never runs at all — no committed-but-unpaid write is
123
+ * possible. `deps.waitUntil`, when supplied (the request's `ctx.waitUntil`),
124
+ * keeps the opt-in receipt sink alive past the response.
83
125
  * @experimental
84
126
  */
85
- type X402ProcedureChargeGate = (request: Request, spec: X402ProcedureSpec, dispatch: () => Promise<Response>) => Promise<Response>;
127
+ type X402ProcedureChargeGate = (request: Request, spec: X402ProcedureSpec, dispatch: () => Promise<Response>, deps?: ChargeHandlerDeps) => Promise<Response>;
86
128
  /**
87
129
  * Build the injectable procedure charge gate for `config`. One initialised
88
130
  * {@link ChargeMiddleware} is memoised per `functionPath` (each bakes that
89
131
  * function's price + `resource`), since `createChargeMiddleware` fetches
90
132
  * facilitator support on first use. A failed init is not cached, so a transient
91
- * facilitator outage retries on the next request.
133
+ * facilitator outage retries on the next request. Settlement runs before
134
+ * `dispatch` (`settleBeforeHandler: true`) since `dispatch` commits the
135
+ * procedure's real mutation — see `createChargeMiddleware`'s `ChargeMiddlewareOptions`.
92
136
  * @experimental
93
137
  */
94
138
  declare const createProcedureChargeGate: (config: X402ProcedureChargeConfig) => X402ProcedureChargeGate;
95
- export { type ChargeHandler, type ChargeMiddleware, type ChargeRouteOverrides, type FacilitatorConfig, type HttpActionHandler, type X402ChargeConfig, type X402Price, type X402ProcedureChargeConfig, type X402ProcedureChargeGate, type X402ProcedureSpec, createChargeMiddleware, createFacilitatorClient, createProcedureChargeGate, withX402 };
139
+ export { type ChargeHandler, type ChargeHandlerDeps, type ChargeMiddleware, type ChargeMiddlewareOptions, type ChargeRouteOverrides, type FacilitatorConfig, type HttpActionHandler, type X402ChargeConfig, type X402Price, type X402ProcedureChargeConfig, type X402ProcedureChargeGate, type X402ProcedureSpec, createChargeMiddleware, createFacilitatorClient, createProcedureChargeGate, withX402 };
@@ -1,7 +1,7 @@
1
1
  export { DEFAULT_FACILITATOR_URL, resolveFacilitatorUrl } from '../packem_shared/DEFAULT_FACILITATOR_URL-Cbz6kIqa.mjs';
2
2
  export { createFacilitatorClient } from '../packem_shared/createFacilitatorClient-rXHBnCZm.mjs';
3
3
  export { isEvmNetwork, isSvmNetwork, toCaip2 } from '../packem_shared/EVM_NETWORKS-BhnYWUQ4.mjs';
4
- export { withX402 } from '../packem_shared/withX402-SOW47gf3.mjs';
5
- export { createChargeMiddleware } from '../packem_shared/createChargeMiddleware-CARkBOyH.mjs';
6
- export { createProcedureChargeGate } from '../packem_shared/createProcedureChargeGate-MaMhu9L9.mjs';
4
+ export { withX402 } from '../packem_shared/withX402-DILL2DvD.mjs';
5
+ export { createChargeMiddleware } from '../packem_shared/createChargeMiddleware-D3yhOpFs.mjs';
6
+ export { createProcedureChargeGate } from '../packem_shared/createProcedureChargeGate-eh9yv36U.mjs';
7
7
  export { toPaymentEventRow, toReceipt } from '../packem_shared/toPaymentEventRow-DW4O9N7Y.mjs';
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, E as EVM_NETWORKS, type a as EvmAddress, type F as FacilitatorConfig, type b as FriendlyNetwork, N as NETWORK_TO_CAIP2, S as SVM_NETWORKS, type X as X402CdpSignerConfig, type c as X402ChargeConfig, type d as X402Network, type e as X402PayConfig, type f as X402Price, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, type j as X402SignerConfig, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2 } from "./packem_shared/config.d-D8gKQLAq.mjs";
1
+ export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, E as EVM_NETWORKS, type a as EvmAddress, type F as FacilitatorConfig, type b as FriendlyNetwork, N as NETWORK_TO_CAIP2, S as SVM_NETWORKS, type X as X402CdpSignerConfig, type c as X402ChargeConfig, type d as X402Network, type e as X402PayConfig, type f as X402Price, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, type j as X402SignerConfig, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2 } from "./packem_shared/config.d-5Nqi5iox.mjs";
2
2
  import '@x402/evm';
3
3
  import '@x402/svm';
4
4
  import '@x402/core/http';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, E as EVM_NETWORKS, type a as EvmAddress, type F as FacilitatorConfig, type b as FriendlyNetwork, N as NETWORK_TO_CAIP2, S as SVM_NETWORKS, type X as X402CdpSignerConfig, type c as X402ChargeConfig, type d as X402Network, type e as X402PayConfig, type f as X402Price, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, type j as X402SignerConfig, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2 } from "./packem_shared/config.d-D8gKQLAq.js";
1
+ export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, E as EVM_NETWORKS, type a as EvmAddress, type F as FacilitatorConfig, type b as FriendlyNetwork, N as NETWORK_TO_CAIP2, S as SVM_NETWORKS, type X as X402CdpSignerConfig, type c as X402ChargeConfig, type d as X402Network, type e as X402PayConfig, type f as X402Price, type g as X402Receipt, type h as X402ReceiptSink, type i as X402Recipient, type j as X402SignerConfig, k as isEvmNetwork, l as isSvmNetwork, r as resolveFacilitatorUrl, t as toCaip2 } from "./packem_shared/config.d-5Nqi5iox.js";
2
2
  import '@x402/evm';
3
3
  import '@x402/svm';
4
4
  import '@x402/core/http';
@@ -10,6 +10,9 @@ const createSpendState = () => {
10
10
  add: (amount) => {
11
11
  spent += amount;
12
12
  },
13
+ release: (amount) => {
14
+ spent = spent > amount ? spent - amount : 0n;
15
+ },
13
16
  get spentAtomic() {
14
17
  return spent;
15
18
  }
@@ -59,27 +62,29 @@ const buildPaymentGuard = (policy, state) => {
59
62
  reason: `x402 policy: this payment (${requirement.amount}) would exceed the per-run cap (already spent ${state.spentAtomic.toString()}, cap ${maxPerRun.toString()}, in atomic base units).`
60
63
  };
61
64
  }
65
+ state.add(amount);
62
66
  if (policy.onPaymentRequired !== void 0) {
63
67
  const approved = await policy.onPaymentRequired(requirement);
64
68
  if (!approved) {
69
+ state.release(amount);
65
70
  return { abort: true, reason: "x402 policy: payment was declined by onPaymentRequired." };
66
71
  }
67
72
  }
68
73
  return void 0;
69
74
  };
70
75
  };
71
- const recordSpend = (state) => (context) => {
72
- state.add(BigInt(context.selectedRequirements.amount));
76
+ const releaseSpendOnFailure = (state) => (context) => {
77
+ state.release(BigInt(context.selectedRequirements.amount));
73
78
  return Promise.resolve();
74
79
  };
75
80
  const assertBoundedPolicy = (policy) => {
76
- const bounded = policy.maxPerCall !== void 0 || policy.maxPerRun !== void 0 || (policy.allowedRecipients?.length ?? 0) > 0 || (policy.allowedNetworks?.length ?? 0) > 0 || policy.onPaymentRequired !== void 0;
81
+ const bounded = policy.maxPerCall !== void 0 || policy.maxPerRun !== void 0 || policy.onPaymentRequired !== void 0;
77
82
  if (!bounded) {
78
83
  throw new LunoraError(
79
84
  "FORBIDDEN",
80
- "x402 pay: refusing to build a wallet with an unbounded spend policy. Set at least one of maxPerCall, maxPerRun, allowedRecipients, allowedNetworks, or onPaymentRequired."
85
+ "x402 pay: refusing to build a wallet with an unbounded spend policy. Set at least one of maxPerCall, maxPerRun, or onPaymentRequired (allowedNetworks/allowedRecipients narrow but do not bound spend)."
81
86
  );
82
87
  }
83
88
  };
84
89
 
85
- export { DEFAULT_STABLECOIN_DECIMALS, assertBoundedPolicy, buildPaymentGuard, buildSpendPolicy, createSpendState, recordSpend, usdToAtomic };
90
+ export { DEFAULT_STABLECOIN_DECIMALS, assertBoundedPolicy, buildPaymentGuard, buildSpendPolicy, createSpendState, releaseSpendOnFailure, usdToAtomic };
@@ -1,7 +1,7 @@
1
1
  import { ClientEvmSigner } from '@x402/evm';
2
2
  import { ClientSvmSigner } from '@x402/svm';
3
3
  import { ProcessSettleSuccessResponse } from '@x402/core/http';
4
- import { BeforePaymentCreationHook, PaymentPolicy, AfterPaymentCreationHook } from '@x402/core/client';
4
+ import { BeforePaymentCreationHook, PaymentPolicy, OnPaymentCreationFailureHook } from '@x402/core/client';
5
5
  import { PaymentRequirements } from '@x402/core/types';
6
6
  /**
7
7
  * A normalised record of one settled x402 payment. The settled `amount` is kept
@@ -193,17 +193,20 @@ interface SpendPolicy {
193
193
  readonly onPaymentRequired?: (requirement: PaymentRequirements) => Promise<boolean> | boolean;
194
194
  }
195
195
  /**
196
- * A running spend ledger the per-run cap is measured against; the guard reads it, the recorder adds to it.
196
+ * A running spend ledger the per-run cap is measured (and reserved) against.
197
197
  * @experimental
198
198
  */
199
199
  interface SpendState {
200
- /** Add a just-committed payment (atomic base units) to the total. */
200
+ /** Reserve a payment (atomic base units) against the running total, before it is signed. */
201
201
  readonly add: (amount: bigint) => void;
202
+ /** Release a previously reserved amount (atomic base units) — e.g. a declined or failed payment. Clamps at 0. */
203
+ readonly release: (amount: bigint) => void;
202
204
  /** Cumulative spend so far, in atomic base units. */
203
205
  readonly spentAtomic: bigint;
204
206
  }
205
207
  /**
206
- * A fresh spend ledger. One per wallet instance; the guard + recorder share it.
208
+ * A fresh spend ledger. One per wallet instance; the guard reserves into it and
209
+ * releases from it.
207
210
  * @experimental
208
211
  */
209
212
  declare const createSpendState: () => SpendState;
@@ -226,19 +229,38 @@ declare const buildSpendPolicy: (policy: SpendPolicy) => PaymentPolicy;
226
229
  * A `BeforePaymentCreationHook` enforcing the stateful bounds the stateless
227
230
  * {@link buildSpendPolicy} filter can't: the cumulative per-run cap and the async
228
231
  * confirmation gate. Aborts (no signature) when either would be violated.
232
+ *
233
+ * The per-run cap is *reserved* into `state` as soon as the check passes — before
234
+ * the `await policy.onPaymentRequired` below, and before `@x402/core` ever attempts
235
+ * to sign — not recorded afterwards. This closes a check-then-act race: without an
236
+ * atomic reserve, N concurrent payments could each read the same `spentAtomic`,
237
+ * all pass the cap check, and all record, overspending the cap by up to
238
+ * (N−1)×maxPerCall. A declined confirmation releases the reservation before this
239
+ * hook returns; {@link releaseSpendOnFailure} releases it if the signature itself
240
+ * later fails. The reservation is intentionally *not* released on success — a
241
+ * committed payment stays counted.
229
242
  * @experimental
230
243
  */
231
244
  declare const buildPaymentGuard: (policy: SpendPolicy, state: SpendState) => BeforePaymentCreationHook;
232
245
  /**
233
- * An `AfterPaymentCreationHook` that adds the just-created payment to `state`, so
234
- * the next {@link buildPaymentGuard} call measures the per-run cap against it.
246
+ * An `OnPaymentCreationFailureHook` that releases a reservation
247
+ * {@link buildPaymentGuard} made when the scheme's signature creation itself
248
+ * throws (network error, wallet error, …) after the guard already approved and
249
+ * reserved the amount. Without this, a failed signature would permanently
250
+ * over-count against the per-run cap for the rest of the run — fail-closed, but
251
+ * needlessly so when the client (`wrapFetchWithPayment`) may retry.
235
252
  * @experimental
236
253
  */
237
- declare const recordSpend: (state: SpendState) => AfterPaymentCreationHook;
254
+ declare const releaseSpendOnFailure: (state: SpendState) => OnPaymentCreationFailureHook;
238
255
  /**
239
256
  * Guard at wallet-build time: refuse a policy with no bound whatsoever. Signing
240
257
  * money on an agent's behalf with unlimited spend authority is never the intent,
241
258
  * so this fails loudly rather than defaulting to unbounded.
259
+ *
260
+ * `allowedNetworks` / `allowedRecipients` narrow *where* a payment can go, but
261
+ * neither caps *how much* — a policy with only an allowlist still authorises
262
+ * unlimited spend to any recipient it permits. Only `maxPerCall`, `maxPerRun`, or
263
+ * a dynamic `onPaymentRequired` gate actually bound spend, so only those count here.
242
264
  * @experimental
243
265
  */
244
266
  declare const assertBoundedPolicy: (policy: SpendPolicy) => void;
@@ -379,4 +401,4 @@ type X402SignerConfig = X402CdpSignerConfig | {
379
401
  * @experimental
380
402
  */
381
403
  declare const resolveFacilitatorUrl: (facilitator?: FacilitatorConfig) => string;
382
- export { Caip2 as C, DEFAULT_FACILITATOR_URL as D, EVM_NETWORKS as E, FacilitatorConfig as F, NETWORK_TO_CAIP2 as N, PaymentEventRow as P, SVM_NETWORKS as S, X402CdpSignerConfig as X, EvmAddress as a, FriendlyNetwork as b, X402ChargeConfig as c, X402Network as d, X402PayConfig as e, X402Price as f, X402Receipt as g, X402ReceiptSink as h, X402Recipient as i, X402SignerConfig as j, isEvmNetwork as k, isSvmNetwork as l, DEFAULT_STABLECOIN_DECIMALS as m, SpendPolicy as n, SpendState as o, assertBoundedPolicy as p, buildPaymentGuard as q, resolveFacilitatorUrl as r, buildSpendPolicy as s, toCaip2 as t, createSpendState as u, recordSpend as v, usdToAtomic as w, toPaymentEventRow as x, toReceipt as y };
404
+ export { Caip2 as C, DEFAULT_FACILITATOR_URL as D, EVM_NETWORKS as E, FacilitatorConfig as F, NETWORK_TO_CAIP2 as N, PaymentEventRow as P, SVM_NETWORKS as S, X402CdpSignerConfig as X, EvmAddress as a, FriendlyNetwork as b, X402ChargeConfig as c, X402Network as d, X402PayConfig as e, X402Price as f, X402Receipt as g, X402ReceiptSink as h, X402Recipient as i, X402SignerConfig as j, isEvmNetwork as k, isSvmNetwork as l, DEFAULT_STABLECOIN_DECIMALS as m, SpendPolicy as n, SpendState as o, assertBoundedPolicy as p, buildPaymentGuard as q, resolveFacilitatorUrl as r, buildSpendPolicy as s, toCaip2 as t, createSpendState as u, releaseSpendOnFailure as v, usdToAtomic as w, toPaymentEventRow as x, toReceipt as y };
@@ -1,7 +1,7 @@
1
1
  import { ClientEvmSigner } from '@x402/evm';
2
2
  import { ClientSvmSigner } from '@x402/svm';
3
3
  import { ProcessSettleSuccessResponse } from '@x402/core/http';
4
- import { BeforePaymentCreationHook, PaymentPolicy, AfterPaymentCreationHook } from '@x402/core/client';
4
+ import { BeforePaymentCreationHook, PaymentPolicy, OnPaymentCreationFailureHook } from '@x402/core/client';
5
5
  import { PaymentRequirements } from '@x402/core/types';
6
6
  /**
7
7
  * A normalised record of one settled x402 payment. The settled `amount` is kept
@@ -193,17 +193,20 @@ interface SpendPolicy {
193
193
  readonly onPaymentRequired?: (requirement: PaymentRequirements) => Promise<boolean> | boolean;
194
194
  }
195
195
  /**
196
- * A running spend ledger the per-run cap is measured against; the guard reads it, the recorder adds to it.
196
+ * A running spend ledger the per-run cap is measured (and reserved) against.
197
197
  * @experimental
198
198
  */
199
199
  interface SpendState {
200
- /** Add a just-committed payment (atomic base units) to the total. */
200
+ /** Reserve a payment (atomic base units) against the running total, before it is signed. */
201
201
  readonly add: (amount: bigint) => void;
202
+ /** Release a previously reserved amount (atomic base units) — e.g. a declined or failed payment. Clamps at 0. */
203
+ readonly release: (amount: bigint) => void;
202
204
  /** Cumulative spend so far, in atomic base units. */
203
205
  readonly spentAtomic: bigint;
204
206
  }
205
207
  /**
206
- * A fresh spend ledger. One per wallet instance; the guard + recorder share it.
208
+ * A fresh spend ledger. One per wallet instance; the guard reserves into it and
209
+ * releases from it.
207
210
  * @experimental
208
211
  */
209
212
  declare const createSpendState: () => SpendState;
@@ -226,19 +229,38 @@ declare const buildSpendPolicy: (policy: SpendPolicy) => PaymentPolicy;
226
229
  * A `BeforePaymentCreationHook` enforcing the stateful bounds the stateless
227
230
  * {@link buildSpendPolicy} filter can't: the cumulative per-run cap and the async
228
231
  * confirmation gate. Aborts (no signature) when either would be violated.
232
+ *
233
+ * The per-run cap is *reserved* into `state` as soon as the check passes — before
234
+ * the `await policy.onPaymentRequired` below, and before `@x402/core` ever attempts
235
+ * to sign — not recorded afterwards. This closes a check-then-act race: without an
236
+ * atomic reserve, N concurrent payments could each read the same `spentAtomic`,
237
+ * all pass the cap check, and all record, overspending the cap by up to
238
+ * (N−1)×maxPerCall. A declined confirmation releases the reservation before this
239
+ * hook returns; {@link releaseSpendOnFailure} releases it if the signature itself
240
+ * later fails. The reservation is intentionally *not* released on success — a
241
+ * committed payment stays counted.
229
242
  * @experimental
230
243
  */
231
244
  declare const buildPaymentGuard: (policy: SpendPolicy, state: SpendState) => BeforePaymentCreationHook;
232
245
  /**
233
- * An `AfterPaymentCreationHook` that adds the just-created payment to `state`, so
234
- * the next {@link buildPaymentGuard} call measures the per-run cap against it.
246
+ * An `OnPaymentCreationFailureHook` that releases a reservation
247
+ * {@link buildPaymentGuard} made when the scheme's signature creation itself
248
+ * throws (network error, wallet error, …) after the guard already approved and
249
+ * reserved the amount. Without this, a failed signature would permanently
250
+ * over-count against the per-run cap for the rest of the run — fail-closed, but
251
+ * needlessly so when the client (`wrapFetchWithPayment`) may retry.
235
252
  * @experimental
236
253
  */
237
- declare const recordSpend: (state: SpendState) => AfterPaymentCreationHook;
254
+ declare const releaseSpendOnFailure: (state: SpendState) => OnPaymentCreationFailureHook;
238
255
  /**
239
256
  * Guard at wallet-build time: refuse a policy with no bound whatsoever. Signing
240
257
  * money on an agent's behalf with unlimited spend authority is never the intent,
241
258
  * so this fails loudly rather than defaulting to unbounded.
259
+ *
260
+ * `allowedNetworks` / `allowedRecipients` narrow *where* a payment can go, but
261
+ * neither caps *how much* — a policy with only an allowlist still authorises
262
+ * unlimited spend to any recipient it permits. Only `maxPerCall`, `maxPerRun`, or
263
+ * a dynamic `onPaymentRequired` gate actually bound spend, so only those count here.
242
264
  * @experimental
243
265
  */
244
266
  declare const assertBoundedPolicy: (policy: SpendPolicy) => void;
@@ -379,4 +401,4 @@ type X402SignerConfig = X402CdpSignerConfig | {
379
401
  * @experimental
380
402
  */
381
403
  declare const resolveFacilitatorUrl: (facilitator?: FacilitatorConfig) => string;
382
- export { Caip2 as C, DEFAULT_FACILITATOR_URL as D, EVM_NETWORKS as E, FacilitatorConfig as F, NETWORK_TO_CAIP2 as N, PaymentEventRow as P, SVM_NETWORKS as S, X402CdpSignerConfig as X, EvmAddress as a, FriendlyNetwork as b, X402ChargeConfig as c, X402Network as d, X402PayConfig as e, X402Price as f, X402Receipt as g, X402ReceiptSink as h, X402Recipient as i, X402SignerConfig as j, isEvmNetwork as k, isSvmNetwork as l, DEFAULT_STABLECOIN_DECIMALS as m, SpendPolicy as n, SpendState as o, assertBoundedPolicy as p, buildPaymentGuard as q, resolveFacilitatorUrl as r, buildSpendPolicy as s, toCaip2 as t, createSpendState as u, recordSpend as v, usdToAtomic as w, toPaymentEventRow as x, toReceipt as y };
404
+ export { Caip2 as C, DEFAULT_FACILITATOR_URL as D, EVM_NETWORKS as E, FacilitatorConfig as F, NETWORK_TO_CAIP2 as N, PaymentEventRow as P, SVM_NETWORKS as S, X402CdpSignerConfig as X, EvmAddress as a, FriendlyNetwork as b, X402ChargeConfig as c, X402Network as d, X402PayConfig as e, X402Price as f, X402Receipt as g, X402ReceiptSink as h, X402Recipient as i, X402SignerConfig as j, isEvmNetwork as k, isSvmNetwork as l, DEFAULT_STABLECOIN_DECIMALS as m, SpendPolicy as n, SpendState as o, assertBoundedPolicy as p, buildPaymentGuard as q, resolveFacilitatorUrl as r, buildSpendPolicy as s, toCaip2 as t, createSpendState as u, releaseSpendOnFailure as v, usdToAtomic as w, toPaymentEventRow as x, toReceipt as y };
@@ -25,13 +25,14 @@ const headerRecord = (headers) => {
25
25
  }
26
26
  return record;
27
27
  };
28
- const reportReceipt = (sink, settlement, resource) => {
28
+ const reportReceipt = (sink, settlement, resource, waitUntil) => {
29
29
  if (sink === void 0) {
30
30
  return;
31
31
  }
32
32
  try {
33
- Promise.resolve(sink(toReceipt(settlement, { resource, ts: Date.now() }))).catch(() => {
33
+ const sent = Promise.resolve(sink(toReceipt(settlement, { resource, ts: Date.now() }))).catch(() => {
34
34
  });
35
+ waitUntil?.(sent);
35
36
  } catch {
36
37
  }
37
38
  };
@@ -102,11 +103,12 @@ const withHeaders = (response, extra) => {
102
103
  }
103
104
  return new Response(response.body, { headers, status: response.status, statusText: response.statusText });
104
105
  };
105
- const createChargeMiddleware = async (config, routeOverrides) => {
106
+ const createChargeMiddleware = async (config, routeOverrides, options) => {
106
107
  const server = await buildResourceServer(config);
107
108
  const http = new x402HTTPResourceServer(server, { ...buildRoute(config), ...routeOverrides });
108
109
  await http.initialize();
109
- const handle = async (request, runHandler) => {
110
+ const settleBeforeHandler = options?.settleBeforeHandler ?? false;
111
+ const handle = async (request, runHandler, deps) => {
110
112
  const url = new URL(request.url);
111
113
  const context = {
112
114
  adapter: createRequestAdapter(request, url),
@@ -121,6 +123,18 @@ const createChargeMiddleware = async (config, routeOverrides) => {
121
123
  if (result.type === "payment-error") {
122
124
  return toResponse(result.response);
123
125
  }
126
+ const resource = routeOverrides?.resource ?? request.url;
127
+ if (settleBeforeHandler) {
128
+ const settlement2 = await http.processSettlement(result.paymentPayload, result.paymentRequirements, result.declaredExtensions, {
129
+ request: context
130
+ });
131
+ if (!settlement2.success) {
132
+ return toResponse(settlement2.response);
133
+ }
134
+ reportReceipt(config.onReceipt, settlement2, resource, deps?.waitUntil);
135
+ const response2 = await runHandler();
136
+ return withHeaders(response2, settlement2.headers);
137
+ }
124
138
  let response;
125
139
  try {
126
140
  response = await runHandler();
@@ -137,7 +151,7 @@ const createChargeMiddleware = async (config, routeOverrides) => {
137
151
  responseHeaders: headerRecord(response.headers)
138
152
  });
139
153
  if (settlement.success) {
140
- reportReceipt(config.onReceipt, settlement, routeOverrides?.resource ?? request.url);
154
+ reportReceipt(config.onReceipt, settlement, resource, deps?.waitUntil);
141
155
  return withHeaders(response, settlement.headers);
142
156
  }
143
157
  return toResponse(settlement.response);
@@ -1,6 +1,6 @@
1
1
  import { x402Client } from '@x402/core/client';
2
2
  import { wrapFetchWithPayment } from '@x402/fetch';
3
- import { assertBoundedPolicy, buildSpendPolicy, buildPaymentGuard, recordSpend, createSpendState } from './DEFAULT_STABLECOIN_DECIMALS-CSu5b5lD.mjs';
3
+ import { assertBoundedPolicy, buildSpendPolicy, buildPaymentGuard, releaseSpendOnFailure, createSpendState } from './DEFAULT_STABLECOIN_DECIMALS-CpW619nu.mjs';
4
4
  import { registerWallet } from './registerWallet-I4pVwq65.mjs';
5
5
 
6
6
  const createPayFetch = async (config, deps) => {
@@ -10,7 +10,7 @@ const createPayFetch = async (config, deps) => {
10
10
  const state = createSpendState();
11
11
  client.registerPolicy(buildSpendPolicy(config.policy));
12
12
  client.onBeforePaymentCreation(buildPaymentGuard(config.policy, state));
13
- client.onAfterPaymentCreation(recordSpend(state));
13
+ client.onPaymentCreationFailure(releaseSpendOnFailure(state));
14
14
  return wrapFetchWithPayment(deps.fetch ?? globalThis.fetch, client);
15
15
  };
16
16
 
@@ -1,18 +1,20 @@
1
- import { createChargeMiddleware } from './createChargeMiddleware-CARkBOyH.mjs';
1
+ import { createChargeMiddleware } from './createChargeMiddleware-D3yhOpFs.mjs';
2
2
 
3
3
  const createProcedureChargeGate = (config) => {
4
4
  const middlewareByFunction = /* @__PURE__ */ new Map();
5
- return async (request, spec, dispatch) => {
5
+ return async (request, spec, dispatch, deps) => {
6
6
  let pending = middlewareByFunction.get(spec.functionPath);
7
7
  if (pending === void 0) {
8
- pending = createChargeMiddleware({ ...config, price: spec.price }, { resource: spec.functionPath }).catch((error) => {
9
- middlewareByFunction.delete(spec.functionPath);
10
- throw error;
11
- });
8
+ pending = createChargeMiddleware({ ...config, price: spec.price }, { resource: spec.functionPath }, { settleBeforeHandler: true }).catch(
9
+ (error) => {
10
+ middlewareByFunction.delete(spec.functionPath);
11
+ throw error;
12
+ }
13
+ );
12
14
  middlewareByFunction.set(spec.functionPath, pending);
13
15
  }
14
16
  const middleware = await pending;
15
- return middleware.handle(request, dispatch);
17
+ return middleware.handle(request, dispatch, deps);
16
18
  };
17
19
  };
18
20
 
@@ -1,4 +1,4 @@
1
- import { createChargeMiddleware } from './createChargeMiddleware-CARkBOyH.mjs';
1
+ import { createChargeMiddleware } from './createChargeMiddleware-D3yhOpFs.mjs';
2
2
 
3
3
  const withX402 = (config, handler) => {
4
4
  let pending;
@@ -1,5 +1,5 @@
1
- import { e as X402PayConfig } from "../packem_shared/config.d-D8gKQLAq.mjs";
2
- export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, m as DEFAULT_STABLECOIN_DECIMALS, type n as SpendPolicy, type o as SpendState, type X as X402CdpSignerConfig, type d as X402Network, type f as X402Price, type j as X402SignerConfig, p as assertBoundedPolicy, q as buildPaymentGuard, s as buildSpendPolicy, u as createSpendState, k as isEvmNetwork, l as isSvmNetwork, v as recordSpend, r as resolveFacilitatorUrl, t as toCaip2, w as usdToAtomic } from "../packem_shared/config.d-D8gKQLAq.mjs";
1
+ import { e as X402PayConfig } from "../packem_shared/config.d-5Nqi5iox.mjs";
2
+ export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, m as DEFAULT_STABLECOIN_DECIMALS, type n as SpendPolicy, type o as SpendState, type X as X402CdpSignerConfig, type d as X402Network, type f as X402Price, type j as X402SignerConfig, p as assertBoundedPolicy, q as buildPaymentGuard, s as buildSpendPolicy, u as createSpendState, k as isEvmNetwork, l as isSvmNetwork, v as releaseSpendOnFailure, r as resolveFacilitatorUrl, t as toCaip2, w as usdToAtomic } from "../packem_shared/config.d-5Nqi5iox.mjs";
3
3
  import { x402Client } from '@x402/core/client';
4
4
  import { ClientSvmSigner } from '@x402/svm';
5
5
  import { PrivateKeyAccount } from 'viem/accounts';
@@ -1,5 +1,5 @@
1
- import { e as X402PayConfig } from "../packem_shared/config.d-D8gKQLAq.js";
2
- export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, m as DEFAULT_STABLECOIN_DECIMALS, type n as SpendPolicy, type o as SpendState, type X as X402CdpSignerConfig, type d as X402Network, type f as X402Price, type j as X402SignerConfig, p as assertBoundedPolicy, q as buildPaymentGuard, s as buildSpendPolicy, u as createSpendState, k as isEvmNetwork, l as isSvmNetwork, v as recordSpend, r as resolveFacilitatorUrl, t as toCaip2, w as usdToAtomic } from "../packem_shared/config.d-D8gKQLAq.js";
1
+ import { e as X402PayConfig } from "../packem_shared/config.d-5Nqi5iox.js";
2
+ export { type C as Caip2, D as DEFAULT_FACILITATOR_URL, m as DEFAULT_STABLECOIN_DECIMALS, type n as SpendPolicy, type o as SpendState, type X as X402CdpSignerConfig, type d as X402Network, type f as X402Price, type j as X402SignerConfig, p as assertBoundedPolicy, q as buildPaymentGuard, s as buildSpendPolicy, u as createSpendState, k as isEvmNetwork, l as isSvmNetwork, v as releaseSpendOnFailure, r as resolveFacilitatorUrl, t as toCaip2, w as usdToAtomic } from "../packem_shared/config.d-5Nqi5iox.js";
3
3
  import { x402Client } from '@x402/core/client';
4
4
  import { ClientSvmSigner } from '@x402/svm';
5
5
  import { PrivateKeyAccount } from 'viem/accounts';
@@ -1,7 +1,7 @@
1
- import { createPayFetch } from '../packem_shared/createPayFetch-O2vkvM1v.mjs';
1
+ import { createPayFetch } from '../packem_shared/createPayFetch-BeT05njL.mjs';
2
2
  export { DEFAULT_FACILITATOR_URL, resolveFacilitatorUrl } from '../packem_shared/DEFAULT_FACILITATOR_URL-Cbz6kIqa.mjs';
3
3
  export { isEvmNetwork, isSvmNetwork, toCaip2 } from '../packem_shared/EVM_NETWORKS-BhnYWUQ4.mjs';
4
- export { DEFAULT_STABLECOIN_DECIMALS, assertBoundedPolicy, buildPaymentGuard, buildSpendPolicy, createSpendState, recordSpend, usdToAtomic } from '../packem_shared/DEFAULT_STABLECOIN_DECIMALS-CSu5b5lD.mjs';
4
+ export { DEFAULT_STABLECOIN_DECIMALS, assertBoundedPolicy, buildPaymentGuard, buildSpendPolicy, createSpendState, releaseSpendOnFailure, usdToAtomic } from '../packem_shared/DEFAULT_STABLECOIN_DECIMALS-CpW619nu.mjs';
5
5
  export { registerWallet, resolveEvmAccount, resolveSvmSigner } from '../packem_shared/registerWallet-I4pVwq65.mjs';
6
6
 
7
7
  const createX402Pay = async (config, deps) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/x402",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "Agentic payments (x402) for Lunora: charge agents per request (charge rail) and let your agents pay x402-gated resources (pay rail)",
5
5
  "keywords": [
6
6
  "agents",