openpay-x402-sdk 0.3.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ - Reserve session and daily capacity immediately before exposing a signed
6
+ authorization. Non-2xx responses, timeouts, and connection failures retain
7
+ the reservation; successful 2xx responses keep the existing confirmed-spend
8
+ accounting.
9
+ - Make the file daily store cross-process atomic with an exclusive lock, reject
10
+ UTC-crossing authorizations, and fail closed when a configured store is
11
+ unavailable.
12
+ - Enforce host/catalog admission before target I/O, block private and rebinding
13
+ destinations, require exact catalog URLs, stop redirects, and bound buyer
14
+ requests with a timeout.
15
+ - Bind supported networks to the canonical JPYC v3 contract/domain, cap
16
+ seller-declared authorization lifetimes, bind signature destinations to a
17
+ known or catalog-reviewed forwarder, and locally reserve seller-gate
18
+ authorizations across verify and settle without requiring facilitator tokens.
19
+ - Verify facilitator-signed payment receipts against the advertised signer and
20
+ bind every money field and authorization nonce before returning them.
21
+
22
+ ## 0.4.0
23
+
24
+ - Add an opt-in persistent daily buyer limit with UTC signer/date keys, file and
25
+ injectable spend stores, quote-time visibility, and fail-closed reads.
26
+ - Record daily spend only after successful 2xx unlocks while isolating store
27
+ write failures from already completed payment responses.
28
+
3
29
  ## 0.3.0
4
30
 
5
31
  - Add `createJpycGate` for seller-side x402 gates backed by the OpenPay catalog,
package/README.md CHANGED
@@ -3,6 +3,11 @@
3
3
  Node.js 20+ SDK for discovering, quoting, and buying OpenPay x402 resources priced
4
4
  in JPYC. It ships as plain ESM and has no build step.
5
5
 
6
+ Wire compatibility: x402 v1 transport (JSON 402 body with `x402Version: 1`, plus the
7
+ `X-PAYMENT` / `X-PAYMENT-RESPONSE` headers) with the OpenPay `extra.openpay`
8
+ forwarder-split extension. OpenPay's first-party resources also accept the v2 header
9
+ transport; this SDK speaks v1.
10
+
6
11
  ## Quick start
7
12
 
8
13
  ```bash
@@ -16,6 +21,8 @@ const client = createOpenPayClient({
16
21
  privateKey: process.env.BUYER_PRIVATE_KEY,
17
22
  maxPerCallJpyc: '10',
18
23
  maxSessionJpyc: '100',
24
+ maxDailyJpyc: '250',
25
+ maxTimeoutSeconds: 600,
19
26
  allowedHosts: 'open-pay.jp',
20
27
  });
21
28
 
@@ -34,6 +41,12 @@ concurrent calls so every call sees the latest session total.
34
41
 
35
42
  ## Sell with the SDK
36
43
 
44
+ **An AI model is never the security boundary for payment state.** Do not unlock on any
45
+ model- or tool-produced claim of payment — a "paid" string, a success message, or a
46
+ transaction id appearing in text. The gate unlocks only on the facilitator's `verify` and
47
+ `settle` responses, which are backed by on-chain settlement; replayed or already-used
48
+ authorizations are refused at that layer.
49
+
37
50
  Create a gate with the exact resource URL registered in OpenPay discovery. For
38
51
  inexpensive content, `handle()` verifies and settles the payment in one call:
39
52
 
@@ -42,6 +55,7 @@ import { createJpycGate } from 'openpay-x402-sdk';
42
55
 
43
56
  const gate = createJpycGate({
44
57
  resourceUrl: process.env.MY_RESOURCE_URL,
58
+ maxUpstreamSeconds: 60,
45
59
  });
46
60
 
47
61
  export async function GET(request) {
@@ -90,6 +104,20 @@ minutes. Until `resourceUrl` is listed with a non-empty `accepts`, `handle()` an
90
104
  `verify()` throw; map that bootstrap condition to an HTTP 500 response. Pass
91
105
  `openpayOrigin` to use an origin other than `https://open-pay.jp`.
92
106
 
107
+ Before `verify()` contacts the facilitator, each gate instance claims a canonical
108
+ authorization identity until its `validBefore` time. Set `maxUpstreamSeconds`
109
+ (default `60`) to the seller's worst-case upstream duration; the gate also
110
+ retains `settlementGraceSeconds` (default `30`) and rejects a duplicate before
111
+ upstream work begins. A failed facilitator verification releases the tentative
112
+ claim, while a successful verification keeps it through settlement so an
113
+ unknown settlement result cannot expose the same authorization twice.
114
+
115
+ The facilitator reservation is an optional additional defense. The gate forwards
116
+ its token when one is returned and continues on the established token-less wire
117
+ when it is not. The local claim covers concurrent requests sharing one gate
118
+ instance; separate processes or serverless isolates do not share its in-memory
119
+ ledger.
120
+
93
121
  The copy-paste paywall snippet generated by OpenPay provides the same one-shot
94
122
  gate; `createJpycGate` is its importable SDK counterpart with split settlement.
95
123
 
@@ -98,22 +126,61 @@ gate; `createJpycGate` is its importable SDK counterpart with split settlement.
98
126
  | Option | Default | Guard |
99
127
  |---|---:|---|
100
128
  | `maxPerCallJpyc` | `10` | Upper bound for the caller-provided `maxTotalJpyc`. |
101
- | `maxSessionJpyc` | `100` | Cumulative cap for successful payments made by this client instance. |
129
+ | `maxSessionJpyc` | `100` | Cumulative cap for successful payments plus authorizations exposed to a seller. |
130
+ | `maxDailyJpyc` | Not set | Persistent cumulative cap per signer and UTC calendar date. |
131
+ | `maxTimeoutSeconds` | `600` | Reject seller-declared authorization lifetimes above this many seconds (maximum configurable value: `1200`). |
102
132
  | `allowedHosts` | `open-pay.jp` | Comma-separated bare host allowlist. |
103
- | `catalogTrust` | `true` | Also allows catalog URLs after the live challenge matches the catalog challenge. |
133
+ | `catalogTrust` | `true` | Also allows exact catalog URLs after the live challenge matches the catalog challenge. |
104
134
  | `discoveryUrl` | `https://open-pay.jp/api/discovery` | Catalog and OpenPay origin used by the client. |
105
135
 
106
- Query string variants of a query-free listed URL are trusted after the same
107
- money-field verification. Exact query-bearing catalog entries remain exact-only.
136
+ Catalog admission is exact URL only, including the query string. A query
137
+ variant must have its own reviewed listing or an explicitly allowlisted host.
108
138
 
109
139
  `pay(url, { maxTotalJpyc })` always requires `maxTotalJpyc`. It is the maximum
110
140
  total—including the resource price and x402 fee—that this individual call is
111
141
  authorized to pay. It does not disable or raise `maxPerCallJpyc` or
112
- `maxSessionJpyc`; all three limits must allow the payment.
142
+ `maxSessionJpyc`; every configured limit must allow the payment.
143
+
144
+ `maxDailyJpyc` is opt-in. When set, the client atomically reserves the amount in
145
+ `~/.openpay-x402/spend.json` immediately before sending a signed authorization,
146
+ keyed by the lower-cased signer address and UTC date. A non-2xx response,
147
+ connection loss, or timeout does not release that reservation: the seller may
148
+ already have settled it. Authorizations that could remain valid across UTC
149
+ midnight are refused instead of being charged to the wrong day.
150
+
151
+ A missing entry starts at zero. A corrupt/unreadable store, a failed reservation,
152
+ or a custom store returning `null` rejects payments with
153
+ `daily_spend_unavailable` (fail-closed). The built-in file store uses an
154
+ exclusive lock plus atomic replacement, so separate processes sharing a signer
155
+ cannot both reserve the same remaining daily capacity. Custom stores can add an
156
+ atomic `reserve(key, amount, limit, reservation)` method; legacy
157
+ `{ load, save }` stores remain accepted with a verified pre-send write.
158
+ `MAX_DAILY_JPYC` is the equivalent optional setting for the exported environment
159
+ config readers.
160
+
161
+ An abrupt process or machine stop can leave `spend.json.lock`; reservations then
162
+ fail closed instead of paying without a limit. Stop every process that uses the
163
+ same store before inspecting and manually removing a stale lock. Never remove
164
+ the lock while another process may still own it.
113
165
 
114
166
  The client also rejects non-JPYC metadata, unsupported networks or schemes,
115
- non-OpenPay forwarder splits, amount inconsistencies, resource URL mismatches,
116
- and catalog bait-and-switches before requesting a signature.
167
+ non-canonical JPYC contracts or EIP-712 domains, seller timeouts above
168
+ `maxTimeoutSeconds`, unreviewed forwarder destinations, non-OpenPay forwarder
169
+ splits, amount inconsistencies,
170
+ resource URL mismatches, and catalog bait-and-switches before requesting a
171
+ signature. `MAX_TIMEOUT_SECONDS` is the equivalent setting for the exported
172
+ environment config readers. Target host/catalog admission and private-address
173
+ checks run before buyer target requests. Those requests require HTTPS, do not
174
+ follow redirects, and have a 15-second timeout. The default Node transport also
175
+ validates DNS before and during connection to block rebinding; an explicitly
176
+ injected custom `fetchImpl` is a trusted transport boundary and is responsible
177
+ for equivalent connect-time enforcement.
178
+
179
+ `pay()` returns a non-null `receipt` only when the facilitator signer advertised
180
+ by `/api/facilitator/supported` signed it and the transaction, payer, network,
181
+ asset, merchant amount, fee, chain, and authorization nonce all match this
182
+ payment. A missing, malformed, forged, or mismatched seller response header
183
+ becomes `receipt: null` without discarding the unlocked response body.
117
184
 
118
185
  ## Signers
119
186
 
package/index.d.ts CHANGED
@@ -37,13 +37,83 @@ export interface PaymentSigner {
37
37
  signTypedData(typedData: PaymentTypedData): Hex | Promise<Hex>;
38
38
  }
39
39
 
40
+ export interface SpendStore {
41
+ /**
42
+ * Return the atomic JPYC spent for the key, `'0'` when no record exists, or
43
+ * `null` only on read failure. `null` fails closed (payments are refused),
44
+ * so an absent entry must be reported as `'0'`, never `null`.
45
+ */
46
+ load(key: string): Promise<string | null>;
47
+ /**
48
+ * Atomically check the limit and reserve an authorization before it is sent.
49
+ * Custom legacy stores may omit this method; new cross-process stores should
50
+ * implement it to avoid lost updates.
51
+ */
52
+ reserve?(
53
+ key: string,
54
+ amountAtomic: string,
55
+ limitAtomic: string,
56
+ reservation: SpendReservation,
57
+ ): Promise<SpendReservationResult>;
58
+ /** Mark reservation metadata confirmed without reducing the reserved total. */
59
+ confirm?(id: string): Promise<boolean>;
60
+ /** Persist the new cumulative atomic amount. Failures must not throw. */
61
+ save(key: string, atomicString: string): Promise<void>;
62
+ }
63
+
64
+ export interface SpendReservation {
65
+ id: string;
66
+ payer: Address;
67
+ network: string;
68
+ asset: Address;
69
+ validBefore: string;
70
+ }
71
+
72
+ export type SpendReservationResult =
73
+ | { ok: true; totalAtomic: string }
74
+ | {
75
+ ok: false;
76
+ reason: 'limit_exceeded' | 'unavailable';
77
+ totalAtomic?: string;
78
+ };
79
+
80
+ export type PaymentLookup = (
81
+ hostname: string,
82
+ ) => Promise<Array<{ address: string; family?: number }>>;
83
+
84
+ export interface FileSpendStoreOptions {
85
+ path?: string;
86
+ fsImpl?: {
87
+ readFile(path: string, encoding: 'utf8'): Promise<string>;
88
+ mkdir(path: string, options: { recursive: true }): Promise<unknown>;
89
+ writeFile(
90
+ path: string,
91
+ data: string,
92
+ encoding: 'utf8',
93
+ ): Promise<unknown>;
94
+ open?(
95
+ path: string,
96
+ flags: 'wx',
97
+ ): Promise<{ close(): Promise<unknown> }>;
98
+ rename?(from: string, to: string): Promise<unknown>;
99
+ unlink?(path: string): Promise<unknown>;
100
+ };
101
+ }
102
+
40
103
  interface ClientCommonOptions {
41
104
  maxPerCallJpyc?: JpycAmount;
42
105
  maxSessionJpyc?: JpycAmount;
106
+ maxDailyJpyc?: JpycAmount;
107
+ maxTimeoutSeconds?: number;
108
+ spendStore?: SpendStore;
43
109
  allowedHosts?: string;
44
110
  catalogTrust?: boolean;
45
111
  discoveryUrl?: string;
46
112
  fetchImpl?: typeof globalThis.fetch;
113
+ lookup?: PaymentLookup;
114
+ requestTimeoutMs?: number;
115
+ nowSec?: () => number;
116
+ now?: () => Date | number;
47
117
  }
48
118
 
49
119
  type NoSignerOptions = {
@@ -85,6 +155,12 @@ export interface RuntimeConfig {
85
155
  stewardSignerSecret: string | null;
86
156
  maxPerCallAtomic: bigint;
87
157
  maxSessionAtomic: bigint;
158
+ maxDailyAtomic: bigint | null;
159
+ /**
160
+ * Maximum seller-declared authorization lifetime. Optional here so existing
161
+ * consumers that construct RuntimeConfig manually remain source-compatible.
162
+ */
163
+ maxTimeoutSeconds?: number;
88
164
  allowedHosts: string[];
89
165
  catalogTrust: boolean;
90
166
  discoveryUrl: string;
@@ -199,6 +275,10 @@ export interface JpycGateOptions {
199
275
  openpayOrigin?: string;
200
276
  fetchImpl?: typeof globalThis.fetch;
201
277
  now?: () => number;
278
+ /** Worst-case seller upstream duration protected by the local claim. Default: 60. */
279
+ maxUpstreamSeconds?: number;
280
+ /** Extra validity retained for settlement after upstream work. Default: 30. */
281
+ settlementGraceSeconds?: number;
202
282
  }
203
283
 
204
284
  export interface JpycGatePaymentResponse {
@@ -219,6 +299,7 @@ export function createJpycGate(options: JpycGateOptions): JpycGate;
219
299
  export const RECEIVE_WITH_AUTHORIZATION_TYPES: {
220
300
  ReceiveWithAuthorization: Array<{ name: string; type: string }>;
221
301
  };
302
+ export const MAX_AUTHORIZATION_TIMEOUT_SECONDS: 1200;
222
303
 
223
304
  export interface NormalizedPaymentRequirements {
224
305
  scheme: 'exact';
@@ -302,6 +383,8 @@ export function paymentPayloadFor(
302
383
  export const JPYC_DECIMALS: 18;
303
384
  export const DEFAULT_MAX_PER_CALL_JPYC: '10';
304
385
  export const DEFAULT_MAX_SESSION_JPYC: '100';
386
+ export const DEFAULT_MAX_TIMEOUT_SECONDS: 600;
387
+ export const MAX_SUPPORTED_TIMEOUT_SECONDS: 1200;
305
388
  export const DEFAULT_ALLOWED_HOSTS: 'open-pay.jp';
306
389
  export const DEFAULT_CATALOG_TRUST: true;
307
390
  export const DEFAULT_DISCOVERY_URL: 'https://open-pay.jp/api/discovery';
@@ -311,8 +394,10 @@ export const REASONS: {
311
394
  unsupportedScheme: 'unsupported_scheme';
312
395
  unsupportedNetwork: 'unsupported_network';
313
396
  invalidOpenpayMode: 'invalid_openpay_mode';
397
+ invalidOpenpayForwarder: 'invalid_openpay_forwarder';
314
398
  amountMismatch: 'amount_mismatch';
315
399
  invalidJpycAsset: 'invalid_jpyc_asset';
400
+ timeoutTooLong: 'timeout_too_long';
316
401
  resourceMismatch: 'resource_mismatch';
317
402
  invalidAccept: 'invalid_accept';
318
403
  maxTotalRequired: 'max_total_required';
@@ -321,12 +406,64 @@ export const REASONS: {
321
406
  maxTotalAbovePerCallLimit: 'max_total_above_per_call_limit';
322
407
  perCallLimitExceeded: 'per_call_limit_exceeded';
323
408
  sessionLimitExceeded: 'session_limit_exceeded';
409
+ dailyLimitExceeded: 'daily_limit_exceeded';
410
+ dailySpendUnavailable: 'daily_spend_unavailable';
411
+ dailyAuthorizationCrossesUtcDay: 'daily_authorization_crosses_utc_day';
324
412
  buyerPrivateKeyMissing: 'buyer_private_key_missing';
325
413
  stewardSignerUnconfigured: 'steward_signer_unconfigured';
326
414
  catalogAcceptMismatch: 'catalog_accept_mismatch';
327
415
  };
416
+ export const SUPPORTED_JPYC_ASSETS: Readonly<
417
+ Record<
418
+ string,
419
+ Readonly<{
420
+ address: Address;
421
+ name: 'JPY Coin';
422
+ version: '1';
423
+ decimals: 18;
424
+ }>
425
+ >
426
+ >;
427
+ export const SUPPORTED_JPYC_FORWARDERS: Readonly<
428
+ Partial<Record<string, Address>>
429
+ >;
328
430
  export const SUPPORTED_NETWORKS: Set<string>;
329
431
 
432
+ export const DEFAULT_PAYMENT_FETCH_TIMEOUT_MS: 15000;
433
+ export function isPrivatePaymentHost(hostname: string): boolean;
434
+ export function parseSafePaymentUrl(raw: unknown): URL | null;
435
+ export function fetchPaymentTarget(
436
+ url: string,
437
+ options?: {
438
+ fetchImpl?: typeof globalThis.fetch;
439
+ headers?: HeadersInit;
440
+ lookup?: PaymentLookup;
441
+ timeoutMs?: number;
442
+ },
443
+ ): Promise<Response>;
444
+
445
+ export type ReceiptSignerResolver = () => Promise<Address | null>;
446
+ export function createReceiptSignerResolver(options: {
447
+ discoveryUrl: string;
448
+ fetchImpl?: typeof globalThis.fetch;
449
+ lookup?: PaymentLookup;
450
+ requestTimeoutMs?: number;
451
+ }): ReceiptSignerResolver;
452
+ export function verifyBoundPaymentResponse(
453
+ paymentResponse: unknown,
454
+ expected: {
455
+ expectedSigner: Address;
456
+ payer: Address;
457
+ network: string;
458
+ asset: Address;
459
+ chainId: number;
460
+ merchant: Address;
461
+ merchantValue: bigint;
462
+ feeValue: bigint;
463
+ nonce: Hex;
464
+ },
465
+ ): Promise<boolean>;
466
+
330
467
  export interface AcceptSummary {
331
468
  priceAtomic: bigint;
332
469
  feeAtomic: bigint;
@@ -358,6 +495,9 @@ export function parseClientOptions(
358
495
  options?: OpenPayClientOptions,
359
496
  ): RuntimeConfig;
360
497
  export function createPaymentSession(initialSpentAtomic?: bigint): PaymentSession;
498
+ export function createFileSpendStore(
499
+ options?: FileSpendStoreOptions,
500
+ ): SpendStore;
361
501
  export function recordSuccessfulPayment(
362
502
  session: PaymentSession,
363
503
  amountAtomic: bigint,
@@ -375,6 +515,7 @@ export function evaluatePaymentGuards(options: {
375
515
  accept: unknown;
376
516
  config: Omit<RuntimeConfig, 'discoveryUrl'> | RuntimeConfig;
377
517
  sessionSpentAtomic?: bigint;
518
+ dailySpentAtomic?: bigint | null;
378
519
  maxTotalJpyc?: JpycAmount;
379
520
  requireMaxTotal?: boolean;
380
521
  requirePrivateKey?: boolean;
@@ -424,12 +565,16 @@ export function createCatalogCache(): CatalogCache;
424
565
  export function resolveCatalogListings(options: {
425
566
  config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
426
567
  fetchImpl?: typeof globalThis.fetch;
568
+ lookup?: PaymentLookup;
569
+ requestTimeoutMs?: number;
427
570
  now?: () => number;
428
571
  cache?: CatalogCache;
429
572
  }): Promise<Map<string, unknown> | null>;
430
573
  export function createCatalogResolver(options: {
431
574
  config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
432
575
  fetchImpl?: typeof globalThis.fetch;
576
+ lookup?: PaymentLookup;
577
+ requestTimeoutMs?: number;
433
578
  now?: () => number;
434
579
  }): () => Promise<Map<string, unknown> | null>;
435
580
 
@@ -444,7 +589,13 @@ export function createPaymentExecutor(options: {
444
589
  config: RuntimeConfig;
445
590
  session: PaymentSession;
446
591
  signer?: PaymentSigner | null;
592
+ signerAddress?: Address | null;
593
+ spendStore?: SpendStore | null;
447
594
  fetchImpl?: typeof globalThis.fetch;
595
+ lookup?: PaymentLookup;
596
+ requestTimeoutMs?: number;
448
597
  nowSec?: () => number;
598
+ now?: () => Date | number;
449
599
  resolveCatalogListings?: () => Promise<Map<string, unknown> | null>;
600
+ resolveReceiptSigner?: ReceiptSignerResolver;
450
601
  }): PaymentExecutor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpay-x402-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Guarded Node.js buyer SDK for OpenPay x402 JPYC resources",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
package/src/catalog.mjs CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ DEFAULT_PAYMENT_FETCH_TIMEOUT_MS,
3
+ fetchPaymentTarget,
4
+ } from './network.mjs';
5
+
1
6
  export const CATALOG_CACHE_MS = 5 * 60_000;
2
7
 
3
8
  function isObject(value) {
@@ -21,6 +26,8 @@ export function createCatalogCache() {
21
26
  export async function resolveCatalogListings({
22
27
  config,
23
28
  fetchImpl = fetch,
29
+ lookup,
30
+ requestTimeoutMs = DEFAULT_PAYMENT_FETCH_TIMEOUT_MS,
24
31
  now = Date.now,
25
32
  cache = createCatalogCache(),
26
33
  }) {
@@ -33,7 +40,10 @@ export async function resolveCatalogListings({
33
40
  }
34
41
 
35
42
  try {
36
- const response = await fetchImpl(config.discoveryUrl, {
43
+ const response = await fetchPaymentTarget(config.discoveryUrl, {
44
+ fetchImpl,
45
+ lookup,
46
+ timeoutMs: requestTimeoutMs,
37
47
  headers: { accept: 'application/json' },
38
48
  });
39
49
  const body = await readJson(response);
@@ -64,7 +74,21 @@ export async function resolveCatalogListings({
64
74
  }
65
75
  }
66
76
 
67
- export function createCatalogResolver({ config, fetchImpl = fetch, now = Date.now }) {
77
+ export function createCatalogResolver({
78
+ config,
79
+ fetchImpl = fetch,
80
+ lookup,
81
+ requestTimeoutMs = DEFAULT_PAYMENT_FETCH_TIMEOUT_MS,
82
+ now = Date.now,
83
+ }) {
68
84
  const cache = createCatalogCache();
69
- return () => resolveCatalogListings({ config, fetchImpl, now, cache });
85
+ return () =>
86
+ resolveCatalogListings({
87
+ config,
88
+ fetchImpl,
89
+ lookup,
90
+ requestTimeoutMs,
91
+ now,
92
+ cache,
93
+ });
70
94
  }
package/src/client.mjs CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  safeErrorMessage,
8
8
  } from './guards.mjs';
9
9
  import { createSignerFromOptions } from './signer.mjs';
10
+ import { createFileSpendStore } from './spendStore.mjs';
11
+ import { createReceiptSignerResolver } from './receipt.mjs';
10
12
 
11
13
  function isObject(value) {
12
14
  return typeof value === 'object' && value !== null;
@@ -63,14 +65,35 @@ export function createOpenPayClient(options = {}) {
63
65
  }
64
66
  const signer = createSignerFromOptions(options, { fetchImpl });
65
67
  const session = createPaymentSession();
66
- const resolveCatalogListings = createCatalogResolver({ config, fetchImpl });
68
+ const spendStore =
69
+ config.maxDailyAtomic === null
70
+ ? null
71
+ : options.spendStore ?? createFileSpendStore();
72
+ const resolveCatalogListings = createCatalogResolver({
73
+ config,
74
+ fetchImpl,
75
+ lookup: options.lookup,
76
+ requestTimeoutMs: options.requestTimeoutMs,
77
+ });
78
+ const resolveReceiptSigner = createReceiptSignerResolver({
79
+ discoveryUrl: config.discoveryUrl,
80
+ fetchImpl,
81
+ lookup: options.lookup,
82
+ requestTimeoutMs: options.requestTimeoutMs,
83
+ });
67
84
  const executor = createPaymentExecutor({
68
85
  config,
69
86
  session,
70
87
  signer,
88
+ signerAddress: signer?.address ?? null,
89
+ spendStore,
71
90
  fetchImpl,
91
+ lookup: options.lookup,
92
+ requestTimeoutMs: options.requestTimeoutMs,
72
93
  nowSec: options.nowSec,
94
+ now: options.now,
73
95
  resolveCatalogListings,
96
+ resolveReceiptSigner,
74
97
  });
75
98
 
76
99
  async function withSafeErrors(operation) {