openpay-x402-sdk 0.4.0 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - Add `createDualGate` — a dual-rail seller gate that serves both JPYC (Polygon,
6
+ OpenPay facilitator) and USDC (Base, standard x402 relayed via OpenPay to the
7
+ CDP facilitator). USDC payments settle directly to the seller wallet with 0%
8
+ OpenPay fee; if the USDC face cannot be fetched, the gate degrades to
9
+ JPYC-only and never blocks JPYC payments.
10
+ - Add `createListingClient` — programmatic marketplace listing (register, list,
11
+ update, deactivate) with built-in SIWE sign-in, so sellers and agents can
12
+ publish listings without the web form. `register` requires an explicit
13
+ `attested: true` (the SDK never attests on your behalf); set `usdc` to also
14
+ appear on the x402 Bazaar after the first settled purchase.
15
+ - Return an explicit `settlement` field from `pay()`, because HTTP `200` only
16
+ means the seller returned a body and is not evidence that the payment settled.
17
+ `verified` means a receipt header was present and the facilitator signature
18
+ bound it to this payment, `unverified` means a header was present but
19
+ unsigned, malformed, forged, or mismatched, and `receipt_unavailable` means no
20
+ header was returned or the facilitator signer could not be resolved. Treat
21
+ anything but `verified` as not proven paid. `receipt` keeps its previous
22
+ meaning and an unlocked response body is still never discarded.
23
+ - Take over a spend lock left behind by a killed process instead of failing
24
+ budgeted payments forever. A lock whose last modification is older than
25
+ `SPEND_LOCK_STALE_MS` (60s, now exported) is moved aside with an atomic
26
+ `rename` — never `unlink` — and the mover re-inspects the moved file to prove
27
+ it took the very lock it measured, so two processes that observe the same
28
+ stale lock cannot both enter the critical section. A lock younger than the
29
+ window is left alone, so a live holder is never displaced.
30
+ - Record the owning `pid` and `createdAt` in the lock file, name the lock path in
31
+ a new `detail` on `{ ok: false, reason: 'unavailable' }`, and treat an
32
+ already-absent lock at release time as a completed release. A custom `fsImpl`
33
+ without `stat`/`rename` keeps the previous fail-closed behavior and now warns
34
+ once instead of disabling the takeover silently.
35
+ - Resolve the target hostname before calling an injected custom `fetchImpl`, so a
36
+ public name pointing at a private or link-local address is rejected before the
37
+ custom transport runs. Connection-time rebinding protection still requires
38
+ supplying `lookup`; a resolver failure does not block, since only the transport
39
+ that opens the socket can re-validate the address it connects to.
40
+ - Require `DISCOVERY_URL` to be `https`, with plaintext `http` allowed only for
41
+ `localhost` / `127.0.0.1`. The discovery origin is the authority for catalog
42
+ trust — URLs it lists are payable without an `ALLOWED_HOSTS` entry — so a
43
+ substitutable plaintext catalog could pass an attacker's resource off as
44
+ reviewed. Both `readRuntimeConfig` and `parseClientOptions` enforce it.
45
+ - Declare the new surface in `index.d.ts` (`SETTLEMENT`, `SettlementStatus`,
46
+ `PaymentResult.settlement`, `SPEND_LOCK_STALE_MS`, `SpendReservationResult.detail`,
47
+ `fsImpl.stat`, `createDualGate`, `createListingClient` and their inputs) and
48
+ document dual-rail selling, code-side listing, settlement truth, stale-lock
49
+ takeover, and the custom-transport boundary in the README.
50
+
51
+ ## 0.5.0
52
+
53
+ - Reserve session and daily capacity immediately before exposing a signed
54
+ authorization. Non-2xx responses, timeouts, and connection failures retain
55
+ the reservation; successful 2xx responses keep the existing confirmed-spend
56
+ accounting.
57
+ - Make the file daily store cross-process atomic with an exclusive lock, reject
58
+ UTC-crossing authorizations, and fail closed when a configured store is
59
+ unavailable.
60
+ - Enforce host/catalog admission before target I/O, block private and rebinding
61
+ destinations, require exact catalog URLs, stop redirects, and bound buyer
62
+ requests with a timeout.
63
+ - Bind supported networks to the canonical JPYC v3 contract/domain, cap
64
+ seller-declared authorization lifetimes, bind signature destinations to a
65
+ known or catalog-reviewed forwarder, and locally reserve seller-gate
66
+ authorizations across verify and settle without requiring facilitator tokens.
67
+ - Verify facilitator-signed payment receipts against the advertised signer and
68
+ bind every money field and authorization nonce before returning them.
69
+
3
70
  ## 0.4.0
4
71
 
5
72
  - Add an opt-in persistent daily buyer limit with UTC signer/date keys, file and
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
@@ -17,6 +22,7 @@ const client = createOpenPayClient({
17
22
  maxPerCallJpyc: '10',
18
23
  maxSessionJpyc: '100',
19
24
  maxDailyJpyc: '250',
25
+ maxTimeoutSeconds: 600,
20
26
  allowedHosts: 'open-pay.jp',
21
27
  });
22
28
 
@@ -35,6 +41,12 @@ concurrent calls so every call sees the latest session total.
35
41
 
36
42
  ## Sell with the SDK
37
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
+
38
50
  Create a gate with the exact resource URL registered in OpenPay discovery. For
39
51
  inexpensive content, `handle()` verifies and settles the payment in one call:
40
52
 
@@ -43,6 +55,7 @@ import { createJpycGate } from 'openpay-x402-sdk';
43
55
 
44
56
  const gate = createJpycGate({
45
57
  resourceUrl: process.env.MY_RESOURCE_URL,
58
+ maxUpstreamSeconds: 60,
46
59
  });
47
60
 
48
61
  export async function GET(request) {
@@ -91,45 +104,147 @@ minutes. Until `resourceUrl` is listed with a non-empty `accepts`, `handle()` an
91
104
  `verify()` throw; map that bootstrap condition to an HTTP 500 response. Pass
92
105
  `openpayOrigin` to use an origin other than `https://open-pay.jp`.
93
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
+
94
121
  The copy-paste paywall snippet generated by OpenPay provides the same one-shot
95
122
  gate; `createJpycGate` is its importable SDK counterpart with split settlement.
96
123
 
124
+ ### Dual-rail: also sell in USDC (Base) and appear on the x402 Bazaar
125
+
126
+ If your listing has the USDC face enabled, use `createDualGate` with the listing
127
+ id (shown as `MY_RESOURCE_ID` in the generated snippet). The 402 then carries
128
+ both JPYC and USDC `accepts` plus a `PAYMENT-REQUIRED` header; USDC payments are
129
+ relayed by OpenPay to the CDP facilitator and settle directly to your Base
130
+ address with 0% OpenPay fee. If the USDC face cannot be fetched (relay off or
131
+ unavailable), the gate degrades to JPYC-only — USDC never blocks JPYC payments.
132
+
133
+ ```js
134
+ import { createDualGate } from 'openpay-x402-sdk';
135
+
136
+ const gate = createDualGate({
137
+ resourceUrl: process.env.MY_RESOURCE_URL,
138
+ resourceId: process.env.MY_RESOURCE_ID,
139
+ });
140
+ ```
141
+
142
+ `handle()` / `verify()` work exactly like `createJpycGate`, on both rails.
143
+
144
+ ### Register listings without the web form
145
+
146
+ `createListingClient` signs in with SIWE and manages your marketplace listings
147
+ programmatically — so a seller (or an agent) can go from nothing to a dual-rail
148
+ listing entirely in code:
149
+
150
+ ```js
151
+ import { createListingClient } from 'openpay-x402-sdk';
152
+
153
+ const listings = createListingClient({ privateKey: process.env.SELLER_PRIVATE_KEY });
154
+ const { resource, paywallSnippet } = await listings.register({
155
+ url: 'https://api.example.com/paid/report',
156
+ description: 'What the purchase completes, in one paragraph.',
157
+ priceJpyc: '100',
158
+ category: 'api',
159
+ usdc: { priceUsd: '0.01', serviceName: 'Example Report API' }, // optional USDC face
160
+ attested: true, // your personal attestation — the SDK never sets this for you
161
+ });
162
+ // resource.id → pass to createDualGate; paywallSnippet → or paste the snippet instead
163
+ ```
164
+
165
+ `register` refuses to run without an explicit `attested: true`: you must
166
+ personally affirm that you have the right to provide and charge for the
167
+ resource and that it is payment-gated (HTTP 402). `list()`, `update(id, input)`,
168
+ and `deactivate(id)` complete the lifecycle; `update` without `usdc` removes the
169
+ USDC face, so pass the previous value to keep it. The private key signs locally
170
+ and is never transmitted.
171
+
97
172
  ## Money guards
98
173
 
99
174
  | Option | Default | Guard |
100
175
  |---|---:|---|
101
176
  | `maxPerCallJpyc` | `10` | Upper bound for the caller-provided `maxTotalJpyc`. |
102
- | `maxSessionJpyc` | `100` | Cumulative cap for successful payments made by this client instance. |
177
+ | `maxSessionJpyc` | `100` | Cumulative cap for successful payments plus authorizations exposed to a seller. |
103
178
  | `maxDailyJpyc` | Not set | Persistent cumulative cap per signer and UTC calendar date. |
179
+ | `maxTimeoutSeconds` | `600` | Reject seller-declared authorization lifetimes above this many seconds (maximum configurable value: `1200`). |
104
180
  | `allowedHosts` | `open-pay.jp` | Comma-separated bare host allowlist. |
105
- | `catalogTrust` | `true` | Also allows catalog URLs after the live challenge matches the catalog challenge. |
181
+ | `catalogTrust` | `true` | Also allows exact catalog URLs after the live challenge matches the catalog challenge. |
106
182
  | `discoveryUrl` | `https://open-pay.jp/api/discovery` | Catalog and OpenPay origin used by the client. |
107
183
 
108
- Query string variants of a query-free listed URL are trusted after the same
109
- money-field verification. Exact query-bearing catalog entries remain exact-only.
184
+ Catalog admission is exact URL only, including the query string. A query
185
+ variant must have its own reviewed listing or an explicitly allowlisted host.
110
186
 
111
187
  `pay(url, { maxTotalJpyc })` always requires `maxTotalJpyc`. It is the maximum
112
188
  total—including the resource price and x402 fee—that this individual call is
113
189
  authorized to pay. It does not disable or raise `maxPerCallJpyc` or
114
190
  `maxSessionJpyc`; every configured limit must allow the payment.
115
191
 
116
- `maxDailyJpyc` is opt-in. When set, the client stores successful 2xx unlocks in
117
- `~/.openpay-x402/spend.json`, keyed by the lower-cased signer address and UTC
118
- date. A missing entry starts at zero. A corrupt/unreadable store or a custom
119
- store returning `null` rejects quotes and payments with `daily_spend_unavailable`
120
- (fail-closed). Use `spendStore` to inject another implementation of
121
- `{ load(key), save(key, atomicString) }`; `MAX_DAILY_JPYC` is the equivalent
122
- optional setting for the exported environment config readers.
123
-
124
- The file store uses best-effort read-modify-write across processes: payments are
125
- serialized within one client process, but separate processes can race and lose
126
- an increment. Use an atomic shared store when multiple processes share a signer.
127
- Persistence runs only after a successful unlock; a save failure cannot change an
128
- already completed payment response.
192
+ `maxDailyJpyc` is opt-in. When set, the client atomically reserves the amount in
193
+ `~/.openpay-x402/spend.json` immediately before sending a signed authorization,
194
+ keyed by the lower-cased signer address and UTC date. A non-2xx response,
195
+ connection loss, or timeout does not release that reservation: the seller may
196
+ already have settled it. Authorizations that could remain valid across UTC
197
+ midnight are refused instead of being charged to the wrong day.
198
+
199
+ A missing entry starts at zero. A corrupt/unreadable store, a failed reservation,
200
+ or a custom store returning `null` rejects payments with
201
+ `daily_spend_unavailable` (fail-closed). The built-in file store uses an
202
+ exclusive lock plus atomic replacement, so separate processes sharing a signer
203
+ cannot both reserve the same remaining daily capacity. Custom stores can add an
204
+ atomic `reserve(key, amount, limit, reservation)` method; legacy
205
+ `{ load, save }` stores remain accepted with a verified pre-send write.
206
+ `MAX_DAILY_JPYC` is the equivalent optional setting for the exported environment
207
+ config readers.
208
+
209
+ An abrupt process or machine stop can leave `spend.json.lock`; reservations then
210
+ fail closed instead of paying without a limit. The lock file records the owning
211
+ `pid` and `createdAt`, and a lock whose last modification is older than 60
212
+ seconds is taken over automatically on the next reservation, so a killed process
213
+ no longer blocks budgeted payments forever. A lock younger than that is left
214
+ alone and the rejection names the lock path in its `detail`. Stop every process
215
+ that uses the same store before removing a lock by hand.
129
216
 
130
217
  The client also rejects non-JPYC metadata, unsupported networks or schemes,
131
- non-OpenPay forwarder splits, amount inconsistencies, resource URL mismatches,
132
- and catalog bait-and-switches before requesting a signature.
218
+ non-canonical JPYC contracts or EIP-712 domains, seller timeouts above
219
+ `maxTimeoutSeconds`, unreviewed forwarder destinations, non-OpenPay forwarder
220
+ splits, amount inconsistencies,
221
+ resource URL mismatches, and catalog bait-and-switches before requesting a
222
+ signature. `MAX_TIMEOUT_SECONDS` is the equivalent setting for the exported
223
+ environment config readers. Target host/catalog admission and private-address
224
+ checks run before buyer target requests. Those requests require HTTPS, do not
225
+ follow redirects, and have a 15-second timeout. The default Node transport also
226
+ validates DNS before and during connection to block rebinding. A custom
227
+ `fetchImpl` still gets the pre-connection resolution check — a hostname that
228
+ resolves to a private or link-local address is rejected before the injected
229
+ transport is called — but connection-time rebinding protection requires also
230
+ supplying `lookup`, because only the transport that opens the socket can
231
+ re-validate the address it actually connects to. A custom `fetchImpl` without
232
+ `lookup` therefore remains a trusted transport boundary for connect-time
233
+ enforcement.
234
+
235
+ `pay()` returns a non-null `receipt` only when the facilitator signer advertised
236
+ by `/api/facilitator/supported` signed it and the transaction, payer, network,
237
+ asset, merchant amount, fee, chain, and authorization nonce all match this
238
+ payment. A missing, malformed, forged, or mismatched seller response header
239
+ becomes `receipt: null` without discarding the unlocked response body.
240
+
241
+ `pay()` also returns an explicit `settlement` field, because an HTTP `200` only
242
+ means the seller returned a body and is not evidence that the payment settled.
243
+ `verified` means a receipt header was present and its facilitator signature was
244
+ bound to this payment; `unverified` means a header was present but unsigned,
245
+ malformed, forged, or mismatched; `receipt_unavailable` means no header was
246
+ returned or the facilitator signer could not be resolved. Treat `unverified` and
247
+ `receipt_unavailable` as not proven paid.
133
248
 
134
249
  ## Signers
135
250
 
package/index.d.ts CHANGED
@@ -44,10 +44,48 @@ export interface SpendStore {
44
44
  * so an absent entry must be reported as `'0'`, never `null`.
45
45
  */
46
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>;
47
60
  /** Persist the new cumulative atomic amount. Failures must not throw. */
48
61
  save(key: string, atomicString: string): Promise<void>;
49
62
  }
50
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
+ * Operator-facing hint for `unavailable`, present only when the block came from the
80
+ * lock file (it names the lock path). Never parse it; it is diagnostic text.
81
+ */
82
+ detail?: string;
83
+ };
84
+
85
+ export type PaymentLookup = (
86
+ hostname: string,
87
+ ) => Promise<Array<{ address: string; family?: number }>>;
88
+
51
89
  export interface FileSpendStoreOptions {
52
90
  path?: string;
53
91
  fsImpl?: {
@@ -58,18 +96,39 @@ export interface FileSpendStoreOptions {
58
96
  data: string,
59
97
  encoding: 'utf8',
60
98
  ): Promise<unknown>;
99
+ open?(
100
+ path: string,
101
+ flags: 'wx',
102
+ ): Promise<{ close(): Promise<unknown> }>;
103
+ rename?(from: string, to: string): Promise<unknown>;
104
+ unlink?(path: string): Promise<unknown>;
105
+ /**
106
+ * Required (together with `rename`) for taking over a lock left behind by a killed
107
+ * process. When either is missing the takeover is disabled and the SDK warns once —
108
+ * a stale lock then blocks budgeted payments until it is removed by hand.
109
+ */
110
+ stat?(path: string): Promise<{ mtimeMs?: number; mtime?: Date | number }>;
61
111
  };
62
112
  }
63
113
 
114
+ /**
115
+ * A spend lock older than this cannot belong to a live holder, so it may be taken over.
116
+ * Exported so operators and tests can reason about the same window as the store.
117
+ */
118
+ export const SPEND_LOCK_STALE_MS: number;
119
+
64
120
  interface ClientCommonOptions {
65
121
  maxPerCallJpyc?: JpycAmount;
66
122
  maxSessionJpyc?: JpycAmount;
67
123
  maxDailyJpyc?: JpycAmount;
124
+ maxTimeoutSeconds?: number;
68
125
  spendStore?: SpendStore;
69
126
  allowedHosts?: string;
70
127
  catalogTrust?: boolean;
71
128
  discoveryUrl?: string;
72
129
  fetchImpl?: typeof globalThis.fetch;
130
+ lookup?: PaymentLookup;
131
+ requestTimeoutMs?: number;
73
132
  nowSec?: () => number;
74
133
  now?: () => Date | number;
75
134
  }
@@ -114,6 +173,11 @@ export interface RuntimeConfig {
114
173
  maxPerCallAtomic: bigint;
115
174
  maxSessionAtomic: bigint;
116
175
  maxDailyAtomic: bigint | null;
176
+ /**
177
+ * Maximum seller-declared authorization lifetime. Optional here so existing
178
+ * consumers that construct RuntimeConfig manually remain source-compatible.
179
+ */
180
+ maxTimeoutSeconds?: number;
117
181
  allowedHosts: string[];
118
182
  catalogTrust: boolean;
119
183
  discoveryUrl: string;
@@ -196,10 +260,30 @@ export interface InvalidChallengeQuote {
196
260
 
197
261
  export type QuoteResult = GuardedQuote | InvalidChallengeQuote;
198
262
 
263
+ /**
264
+ * Settlement truth for one paid call. `status: 200` alone is not evidence of settlement.
265
+ * - `verified`: a receipt header was present and the facilitator signature bound it to this payment.
266
+ * - `unverified`: a receipt header was present but unsigned, malformed, forged, or mismatched.
267
+ * - `receipt_unavailable`: no receipt header, or the facilitator signer could not be resolved.
268
+ */
269
+ export type SettlementStatus = 'verified' | 'unverified' | 'receipt_unavailable';
270
+
271
+ /** The `SettlementStatus` values as a runtime object (`unavailable` = `receipt_unavailable`). */
272
+ export const SETTLEMENT: {
273
+ readonly verified: 'verified';
274
+ readonly unverified: 'unverified';
275
+ readonly unavailable: 'receipt_unavailable';
276
+ };
277
+
199
278
  export interface PaymentResult {
200
279
  status: number;
201
280
  body: unknown;
202
281
  receipt: unknown;
282
+ /**
283
+ * Optional so objects built before this field existed stay source-compatible. Absent must be
284
+ * read the same way as `receipt_unavailable`: no settlement was proven.
285
+ */
286
+ settlement?: SettlementStatus;
203
287
  }
204
288
 
205
289
  export interface OpenPayClient {
@@ -228,6 +312,10 @@ export interface JpycGateOptions {
228
312
  openpayOrigin?: string;
229
313
  fetchImpl?: typeof globalThis.fetch;
230
314
  now?: () => number;
315
+ /** Worst-case seller upstream duration protected by the local claim. Default: 60. */
316
+ maxUpstreamSeconds?: number;
317
+ /** Extra validity retained for settlement after upstream work. Default: 30. */
318
+ settlementGraceSeconds?: number;
231
319
  }
232
320
 
233
321
  export interface JpycGatePaymentResponse {
@@ -245,9 +333,98 @@ export interface JpycGate {
245
333
 
246
334
  export function createJpycGate(options: JpycGateOptions): JpycGate;
247
335
 
336
+ export interface DualGateOptions extends JpycGateOptions {
337
+ /** OpenPay listing id (MY_RESOURCE_ID in the generated snippet). Enables the USDC (Base) rail. */
338
+ resourceId: string;
339
+ }
340
+
341
+ /**
342
+ * Dual-rail seller gate: JPYC (Polygon, OpenPay facilitator) plus USDC (Base, standard x402
343
+ * relayed to the CDP facilitator via OpenPay). If the USDC face cannot be fetched (relay off
344
+ * or unavailable), the gate degrades to JPYC-only — the USDC side never blocks JPYC payments.
345
+ */
346
+ export function createDualGate(options: DualGateOptions): JpycGate;
347
+
348
+ export interface ListingUsdcInput {
349
+ /** USD price without a $ sign, up to 6 decimals (e.g. "0.005"). */
350
+ priceUsd: string;
351
+ /** USDC (Base) receiving address. Defaults to the JPYC payTo. */
352
+ payTo?: string;
353
+ /** Display name for x402 Bazaar search (max 60 chars). */
354
+ serviceName?: string;
355
+ }
356
+
357
+ export interface ListingInput {
358
+ url: string;
359
+ description: string;
360
+ /** Integer JPYC price as a string (e.g. "100"). */
361
+ priceJpyc: string;
362
+ category: string;
363
+ docsUrl?: string;
364
+ license?: string;
365
+ /** JPYC receiving address. Defaults to the signed-in wallet. */
366
+ payTo?: string;
367
+ /** Enable the USDC (Base) face — also lists on the x402 Bazaar after the first settle. */
368
+ usdc?: ListingUsdcInput;
369
+ }
370
+
371
+ export interface RegisterListingInput extends ListingInput {
372
+ /**
373
+ * Required, must be literally true: your personal attestation that you have the right to
374
+ * provide and charge for this resource and that it is payment-gated (HTTP 402).
375
+ * The SDK never sets this for you.
376
+ */
377
+ attested: true;
378
+ }
379
+
380
+ export interface ListingRecord {
381
+ id: string;
382
+ url: string;
383
+ description: string;
384
+ priceJpyc: string;
385
+ category: string;
386
+ payTo: string;
387
+ docsUrl?: string;
388
+ license?: string;
389
+ usdc?: { payTo: string; priceUsd: string; serviceName?: string };
390
+ paywallSnippet?: string;
391
+ hidden?: boolean;
392
+ }
393
+
394
+ export interface RegisterListingResult {
395
+ resource: ListingRecord;
396
+ /** Copy-paste 402 gate for your server (dual-rail x402Gate when usdc is set). */
397
+ paywallSnippet: string;
398
+ }
399
+
400
+ export interface ListingClient {
401
+ /** The seller wallet address that signs in via SIWE (checksummed). */
402
+ address: Address;
403
+ register(input: RegisterListingInput): Promise<RegisterListingResult>;
404
+ list(): Promise<ListingRecord[]>;
405
+ update(id: string, input: ListingInput): Promise<{ resource: ListingRecord }>;
406
+ deactivate(id: string): Promise<boolean>;
407
+ }
408
+
409
+ /**
410
+ * Programmatic listing client — register, list, update, and deactivate OpenPay marketplace
411
+ * listings without the web form. Signs in with SIWE using the given private key on first use;
412
+ * the key is only used to sign locally and is never transmitted.
413
+ */
414
+ export function createListingClient(options: {
415
+ privateKey: string;
416
+ openpayOrigin?: string;
417
+ fetchImpl?: typeof globalThis.fetch;
418
+ /** SIWE chainId (default 137 = Polygon). */
419
+ chainId?: number;
420
+ statement?: string;
421
+ now?: () => number;
422
+ }): ListingClient;
423
+
248
424
  export const RECEIVE_WITH_AUTHORIZATION_TYPES: {
249
425
  ReceiveWithAuthorization: Array<{ name: string; type: string }>;
250
426
  };
427
+ export const MAX_AUTHORIZATION_TIMEOUT_SECONDS: 1200;
251
428
 
252
429
  export interface NormalizedPaymentRequirements {
253
430
  scheme: 'exact';
@@ -331,6 +508,8 @@ export function paymentPayloadFor(
331
508
  export const JPYC_DECIMALS: 18;
332
509
  export const DEFAULT_MAX_PER_CALL_JPYC: '10';
333
510
  export const DEFAULT_MAX_SESSION_JPYC: '100';
511
+ export const DEFAULT_MAX_TIMEOUT_SECONDS: 600;
512
+ export const MAX_SUPPORTED_TIMEOUT_SECONDS: 1200;
334
513
  export const DEFAULT_ALLOWED_HOSTS: 'open-pay.jp';
335
514
  export const DEFAULT_CATALOG_TRUST: true;
336
515
  export const DEFAULT_DISCOVERY_URL: 'https://open-pay.jp/api/discovery';
@@ -340,8 +519,10 @@ export const REASONS: {
340
519
  unsupportedScheme: 'unsupported_scheme';
341
520
  unsupportedNetwork: 'unsupported_network';
342
521
  invalidOpenpayMode: 'invalid_openpay_mode';
522
+ invalidOpenpayForwarder: 'invalid_openpay_forwarder';
343
523
  amountMismatch: 'amount_mismatch';
344
524
  invalidJpycAsset: 'invalid_jpyc_asset';
525
+ timeoutTooLong: 'timeout_too_long';
345
526
  resourceMismatch: 'resource_mismatch';
346
527
  invalidAccept: 'invalid_accept';
347
528
  maxTotalRequired: 'max_total_required';
@@ -352,12 +533,62 @@ export const REASONS: {
352
533
  sessionLimitExceeded: 'session_limit_exceeded';
353
534
  dailyLimitExceeded: 'daily_limit_exceeded';
354
535
  dailySpendUnavailable: 'daily_spend_unavailable';
536
+ dailyAuthorizationCrossesUtcDay: 'daily_authorization_crosses_utc_day';
355
537
  buyerPrivateKeyMissing: 'buyer_private_key_missing';
356
538
  stewardSignerUnconfigured: 'steward_signer_unconfigured';
357
539
  catalogAcceptMismatch: 'catalog_accept_mismatch';
358
540
  };
541
+ export const SUPPORTED_JPYC_ASSETS: Readonly<
542
+ Record<
543
+ string,
544
+ Readonly<{
545
+ address: Address;
546
+ name: 'JPY Coin';
547
+ version: '1';
548
+ decimals: 18;
549
+ }>
550
+ >
551
+ >;
552
+ export const SUPPORTED_JPYC_FORWARDERS: Readonly<
553
+ Partial<Record<string, Address>>
554
+ >;
359
555
  export const SUPPORTED_NETWORKS: Set<string>;
360
556
 
557
+ export const DEFAULT_PAYMENT_FETCH_TIMEOUT_MS: 15000;
558
+ export function isPrivatePaymentHost(hostname: string): boolean;
559
+ export function parseSafePaymentUrl(raw: unknown): URL | null;
560
+ export function fetchPaymentTarget(
561
+ url: string,
562
+ options?: {
563
+ fetchImpl?: typeof globalThis.fetch;
564
+ headers?: HeadersInit;
565
+ lookup?: PaymentLookup;
566
+ timeoutMs?: number;
567
+ },
568
+ ): Promise<Response>;
569
+
570
+ export type ReceiptSignerResolver = () => Promise<Address | null>;
571
+ export function createReceiptSignerResolver(options: {
572
+ discoveryUrl: string;
573
+ fetchImpl?: typeof globalThis.fetch;
574
+ lookup?: PaymentLookup;
575
+ requestTimeoutMs?: number;
576
+ }): ReceiptSignerResolver;
577
+ export function verifyBoundPaymentResponse(
578
+ paymentResponse: unknown,
579
+ expected: {
580
+ expectedSigner: Address;
581
+ payer: Address;
582
+ network: string;
583
+ asset: Address;
584
+ chainId: number;
585
+ merchant: Address;
586
+ merchantValue: bigint;
587
+ feeValue: bigint;
588
+ nonce: Hex;
589
+ },
590
+ ): Promise<boolean>;
591
+
361
592
  export interface AcceptSummary {
362
593
  priceAtomic: bigint;
363
594
  feeAtomic: bigint;
@@ -459,12 +690,16 @@ export function createCatalogCache(): CatalogCache;
459
690
  export function resolveCatalogListings(options: {
460
691
  config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
461
692
  fetchImpl?: typeof globalThis.fetch;
693
+ lookup?: PaymentLookup;
694
+ requestTimeoutMs?: number;
462
695
  now?: () => number;
463
696
  cache?: CatalogCache;
464
697
  }): Promise<Map<string, unknown> | null>;
465
698
  export function createCatalogResolver(options: {
466
699
  config: Pick<RuntimeConfig, 'catalogTrust' | 'discoveryUrl'>;
467
700
  fetchImpl?: typeof globalThis.fetch;
701
+ lookup?: PaymentLookup;
702
+ requestTimeoutMs?: number;
468
703
  now?: () => number;
469
704
  }): () => Promise<Map<string, unknown> | null>;
470
705
 
@@ -482,7 +717,10 @@ export function createPaymentExecutor(options: {
482
717
  signerAddress?: Address | null;
483
718
  spendStore?: SpendStore | null;
484
719
  fetchImpl?: typeof globalThis.fetch;
720
+ lookup?: PaymentLookup;
721
+ requestTimeoutMs?: number;
485
722
  nowSec?: () => number;
486
723
  now?: () => Date | number;
487
724
  resolveCatalogListings?: () => Promise<Map<string, unknown> | null>;
725
+ resolveReceiptSigner?: ReceiptSignerResolver;
488
726
  }): PaymentExecutor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpay-x402-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Guarded Node.js buyer SDK for OpenPay x402 JPYC resources",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",