@genesis-tech/genesispay-seller 0.13.2 → 1.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +719 -91
  3. package/dist/checkout-documents.d.ts +105 -0
  4. package/dist/checkout-documents.d.ts.map +1 -0
  5. package/dist/checkout-documents.js +171 -0
  6. package/dist/checkout-documents.js.map +1 -0
  7. package/dist/checkout-return.d.ts +5 -5
  8. package/dist/checkout-return.d.ts.map +1 -1
  9. package/dist/checkout-return.js +3 -2
  10. package/dist/checkout-return.js.map +1 -1
  11. package/dist/client.d.ts +57 -7
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/client.js +100 -10
  14. package/dist/client.js.map +1 -1
  15. package/dist/contract-mandates.d.ts +39 -0
  16. package/dist/contract-mandates.d.ts.map +1 -0
  17. package/dist/contract-mandates.js +98 -0
  18. package/dist/contract-mandates.js.map +1 -0
  19. package/dist/contract-usage.d.ts +29 -0
  20. package/dist/contract-usage.d.ts.map +1 -0
  21. package/dist/contract-usage.js +92 -0
  22. package/dist/contract-usage.js.map +1 -0
  23. package/dist/errors.d.ts +71 -5
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +124 -13
  26. package/dist/errors.js.map +1 -1
  27. package/dist/fulfillment-attempts.d.ts +29 -0
  28. package/dist/fulfillment-attempts.d.ts.map +1 -0
  29. package/dist/fulfillment-attempts.js +71 -0
  30. package/dist/fulfillment-attempts.js.map +1 -0
  31. package/dist/fulfillment.d.ts +152 -0
  32. package/dist/fulfillment.d.ts.map +1 -0
  33. package/dist/fulfillment.js +810 -0
  34. package/dist/fulfillment.js.map +1 -0
  35. package/dist/genesispay-settlement.d.ts +4 -2
  36. package/dist/genesispay-settlement.d.ts.map +1 -1
  37. package/dist/genesispay-settlement.js +412 -15
  38. package/dist/genesispay-settlement.js.map +1 -1
  39. package/dist/index.d.ts +11 -5
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +7 -4
  42. package/dist/index.js.map +1 -1
  43. package/dist/invoices.d.ts +31 -0
  44. package/dist/invoices.d.ts.map +1 -1
  45. package/dist/invoices.js +61 -2
  46. package/dist/invoices.js.map +1 -1
  47. package/dist/item-tax.d.ts +25 -0
  48. package/dist/item-tax.d.ts.map +1 -0
  49. package/dist/item-tax.js +43 -0
  50. package/dist/item-tax.js.map +1 -0
  51. package/dist/mandate-gate.d.ts +6 -1
  52. package/dist/mandate-gate.d.ts.map +1 -1
  53. package/dist/mandate-gate.js +81 -7
  54. package/dist/mandate-gate.js.map +1 -1
  55. package/dist/mandates.d.ts +21 -4
  56. package/dist/mandates.d.ts.map +1 -1
  57. package/dist/mandates.js +24 -3
  58. package/dist/mandates.js.map +1 -1
  59. package/dist/networks.d.ts.map +1 -1
  60. package/dist/networks.js +5 -4
  61. package/dist/networks.js.map +1 -1
  62. package/dist/payment-gate.d.ts +32 -1
  63. package/dist/payment-gate.d.ts.map +1 -1
  64. package/dist/payment-gate.js +275 -11
  65. package/dist/payment-gate.js.map +1 -1
  66. package/dist/products.d.ts +40 -16
  67. package/dist/products.d.ts.map +1 -1
  68. package/dist/products.js +1078 -56
  69. package/dist/products.js.map +1 -1
  70. package/dist/strict-response.d.ts +21 -0
  71. package/dist/strict-response.d.ts.map +1 -0
  72. package/dist/strict-response.js +68 -0
  73. package/dist/strict-response.js.map +1 -0
  74. package/dist/webhooks.d.ts +77 -2
  75. package/dist/webhooks.d.ts.map +1 -1
  76. package/dist/webhooks.js +20 -0
  77. package/dist/webhooks.js.map +1 -1
  78. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @genesis-tech/genesispay-seller
2
2
 
3
- Framework-agnostic x402 payment gate for sellers, powered by GenesisPay (formerly GenesisPay).
3
+ Framework-agnostic payment and x402 SDK for GenesisPay sellers.
4
4
 
5
5
  Wrap any Web-standard `(Request) => Response` handler and it becomes a paid
6
6
  endpoint:
@@ -15,13 +15,46 @@ endpoint:
15
15
  Works with Next.js route handlers, Hono, Bun.serve, and anything else that
16
16
  speaks the Fetch API.
17
17
 
18
+ Synchronous facilitator success is also fail-closed. The built-in adapter
19
+ requires a full transaction hash, the exact requested network and integer minor
20
+ amount, the exact payer when the signed payload identifies one, and explicit
21
+ `authorizationVerified: true` plus `settlementVerified: true` extensions. An
22
+ HTTP 2xx response or `success: true` without that complete evidence never runs
23
+ the paid handler.
24
+
25
+ When GenesisPay accepts a facilitator settlement asynchronously (HTTP 202), the
26
+ SDK requires a version-1 acceptance for the exact requested plan and, for the
27
+ generic facilitator flow, its canonical plan status URL. Every later status
28
+ body must repeat that same version and plan before the SDK trusts it. The SDK
29
+ polls the seller-authenticated status endpoint internally. Your paid handler
30
+ still runs only after the seller transfer is receipt-verified and has a real
31
+ transaction hash. Queue acceptance and a hash-bearing but unconfirmed
32
+ `submitted` state never release the resource. The default poll deadline is 120
33
+ seconds; set `settlementPollTimeoutMs` on `genesisPaySettlement` when a resource
34
+ server needs a smaller bound. A terminal `failed` or `expired` poll returns HTTP
35
+ 409 or 410 respectively, never a 2xx resource response and never a
36
+ `PAYMENT-RESPONSE` settlement receipt. Once any valid poll reports submitted
37
+ transaction hash H, later malformed, unavailable, terminal or contradictory
38
+ polls retain H as outcome-unknown evidence; the SDK never weakens it into a
39
+ hashless retry path or replaces it with a conflicting hash. A valid payer-
40
+ broadcast hash is retained the same way when the initial 202 acceptance is
41
+ malformed or polling fails before the server reports a transaction hash. A
42
+ replayed acceptance is checked once even after its issuance window, so durable
43
+ submitted/settled evidence remains discoverable; otherwise queued polling stops
44
+ at that acceptance deadline. Once an exact-plan submitted hash is observed,
45
+ reconciliation continues for the configured local polling budget.
46
+
18
47
  ## Install
19
48
 
49
+ This README documents **1.5.0**, which requires protocol `^0.5.0`. Hosted
50
+ checkout `lineItems` and externally calculated order tax need a GenesisPay
51
+ deployment that accepts them.
52
+
20
53
  ```bash
21
- npm install @genesis-tech/genesispay-seller
54
+ npm install @genesis-tech/genesispay-seller@1.5.0
22
55
  ```
23
56
 
24
- ## Quick start (legacy `GenesisPay` client — recommended)
57
+ ## Quick start
25
58
 
26
59
  The `GenesisPay` client configures from **just the seller API key**. The key prefix
27
60
  (`gp_sk_test_…` / `gp_sk_live_…`) determines the mode and base URL, and the payout
@@ -35,18 +68,23 @@ const genesispay = new GenesisPay({ apiKey: process.env.GENESISPAY_SELLER_API_KE
35
68
  // Human hosted checkout — the wallet is defaulted server-side from the key.
36
69
  // `metadata` and `clientReferenceId` come back on retrieve() and on the webhook,
37
70
  // so you never need your own table just to map a link back to a buyer:
38
- const { publicId, payUrl } = await genesispay.checkout.create({
39
- title: "50 credits",
40
- amount: "5.00", // decimal string in `asset` — see "Amounts and assets" below
41
- clientReferenceId: order.id,
42
- metadata: { buyerId: user.id, plan: "starter" },
43
- returnUrl: "https://shop.example.com/thanks",
44
- cancelUrl: "https://shop.example.com/cart",
45
- });
71
+ const { publicId, payUrl } = await genesispay.checkout.create(
72
+ {
73
+ title: "50 credits",
74
+ amount: "5.00", // decimal string in `asset` — see "Amounts and assets" below
75
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 2000, note: null },
76
+ clientReferenceId: order.id,
77
+ metadata: { buyerId: user.id, plan: "starter" },
78
+ returnUrl: "https://shop.example.com/thanks",
79
+ cancelUrl: "https://shop.example.com/cart",
80
+ },
81
+ { idempotencyKey: `checkout-${order.id}` },
82
+ );
46
83
 
47
- // Poll for settlement:
84
+ // Tolerant checkout views are for display and polling only. They are not
85
+ // fulfilment authority; use fulfillment.verify as shown below.
48
86
  const session = await genesispay.checkout.retrieve(publicId);
49
- if (session.paid) fulfil(session.clientReferenceId);
87
+ renderPaymentStatus(session.paid);
50
88
  // An unknown id throws GenesisPayNotFoundError, not GenesisPayConfigError — so a
51
89
  // typo'd id is distinguishable from a broken key or an unreachable backend.
52
90
 
@@ -56,18 +94,125 @@ export const GET = genesispay.gate({ amountUsdc: "0.02" }).wrap(
56
94
  );
57
95
  ```
58
96
 
59
- ### Return URL — parse the hint, then verify
97
+ ### Strict fulfilment authority (1.0)
98
+
99
+ Fulfil only after `genesispay.fulfillment.verify()` returns `verified: true`.
100
+ The method retrieves seller-scoped evidence from GenesisPay, parses every known
101
+ authority field strictly, validates the Base asset deployment, and compares it
102
+ with your immutable expected contract. It does not accept an evidence object,
103
+ so a fabricated SDK-shaped value cannot authorize fulfilment.
104
+
105
+ ```ts
106
+ import {
107
+ GenesisPay,
108
+ GenesisPayContractMismatchError,
109
+ GenesisPayEvidenceError,
110
+ GenesisPayVersionError,
111
+ type ExpectedProductContract,
112
+ } from "@genesis-tech/genesispay-seller";
113
+
114
+ const genesispay = new GenesisPay({
115
+ apiKey: process.env.GENESISPAY_SELLER_API_KEY!,
116
+ });
117
+
118
+ const expected: ExpectedProductContract = {
119
+ kind: "product",
120
+ productId: "prod_starter",
121
+ sku: "starter-credits",
122
+ grossAmountMinor: 1_000_000n,
123
+ network: {
124
+ mode: "live",
125
+ network: "base",
126
+ chainId: 8453,
127
+ asset: "USDC",
128
+ tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
129
+ minorUnitScale: 6,
130
+ },
131
+ settlementDestination: process.env.GENESISPAY_EXPECTED_PAY_TO! as `0x${string}`,
132
+ delivery: {
133
+ type: "url",
134
+ url: "https://shop.example.com/deliver/starter",
135
+ gate: null,
136
+ },
137
+ };
138
+
139
+ try {
140
+ const result = await genesispay.fulfillment.verify({
141
+ // Redirect/webhook entitlement ID is only a locator, never proof itself.
142
+ locator: { entitlementId },
143
+ expected,
144
+ });
145
+
146
+ if (result.verified) {
147
+ await fulfilOnce(result.payment.attemptId);
148
+ } else {
149
+ // not_found, not_confirmed, simulated, or entitlement_invalid are normal
150
+ // negative outcomes. None authorizes fulfilment.
151
+ logPaymentPending(result.reason, result.requestId);
152
+ }
153
+ } catch (error) {
154
+ if (
155
+ error instanceof GenesisPayContractMismatchError ||
156
+ error instanceof GenesisPayEvidenceError ||
157
+ error instanceof GenesisPayVersionError
158
+ ) {
159
+ alertPaymentAuthorityFailure({
160
+ code: error.code,
161
+ requestId: error.requestId,
162
+ apiVersion: error.apiVersion,
163
+ mismatches:
164
+ error instanceof GenesisPayContractMismatchError ? error.mismatches : undefined,
165
+ });
166
+ }
167
+ throw error;
168
+ }
169
+ ```
170
+
171
+ The strict API is explicitly versioned with
172
+ `GENESISPAY-Version: 2026-08-26`; the SDK sends that header and requires the
173
+ same version plus `GENESISPAY-Request-Id` in the response headers and body.
174
+ Amounts cross the wire as bounded canonical decimal strings and become
175
+ `bigint` only after validation. `simulated: false` is required literally and
176
+ is accepted only with the supported authority provenance version.
177
+
178
+ `2026-08-26` is a stable protocol identifier, not the SDK release date. Updating
179
+ the SDK package version does not change stored payment provenance.
180
+
181
+ For account-bound or per-order delivery, store the created `checkout.linkId`
182
+ alongside your order and buyer. After verification, require that the verified
183
+ link belongs to that order and authenticated buyer, and atomically claim the
184
+ attempt before granting credits or goods. A matching product contract alone
185
+ does not bind a payment to the currently signed-in customer.
186
+
187
+ Fee quotes and fee deductions are different: a `record_only` quote may equal
188
+ the gross amount, while the seller still received full gross. A quote above
189
+ gross is invalid, and a `payer_authorized` fee must leave a positive seller
190
+ amount. Never deduct a record-only quote from the verified seller amount.
191
+
192
+ For URL delivery, use the entitlement locator as above: it also checks current
193
+ expiry and revocation. An attempt or link locator proves the immutable payment
194
+ even after an entitlement is revoked or expires, for reconciliation. If you use
195
+ one of those locators to release URL delivery, also require a non-null
196
+ `result.payment.entitlement` with `valid === true` before fulfilling.
197
+
198
+ Use a key whose mode matches the payment chain: `test` for Base Sepolia and
199
+ `live` for Base mainnet. Strict direct/product checkout link creation/replay and
200
+ product-gate
201
+ requests reject `409 seller_mode_mismatch` before exposing or settling a payment
202
+ the same key cannot verify. A `baseUrl` override does not change key mode.
203
+
204
+ ### Return URL — parse the hint, then verify authority
60
205
 
61
206
  When you pass `returnUrl` to `checkout.create`, the hosted checkout shows a
62
207
  "Return to …" button and appends
63
208
  `?genesispay_link_id=<publicId>&genesispay_status=paid` only when the payer
64
209
  selects it — there is no timed auto-redirect. Those parameters are a **UI hint
65
210
  only**: they are unsigned, and a payer can navigate to that URL directly without
66
- paying. **Never fulfil on the query string**; use webhooks as the reliable
67
- delivery path.
211
+ paying. **Never fulfil on the query string**; use authenticated webhooks as a
212
+ trigger and strict verification as authority.
68
213
 
69
- Use `parseCheckoutReturnHint` to read the hint, then confirm with an
70
- **authenticated** `checkout.retrieve()` before fulfilling:
214
+ Use `parseCheckoutReturnHint` only to locate the payment, then call the strict
215
+ seller-authenticated verifier with the expected immutable contract:
71
216
 
72
217
  ```ts
73
218
  import { parseCheckoutReturnHint } from "@genesis-tech/genesispay-seller";
@@ -76,15 +221,11 @@ export async function GET(request: Request) {
76
221
  const hint = parseCheckoutReturnHint(new URL(request.url));
77
222
  if (!hint) return Response.json({ ok: true }); // no return params — nothing to do
78
223
 
79
- // hint.linkId is the link's publicId. Verify with your seller key, not the URL.
80
- const session = await genesispay.checkout.retrieve(hint.linkId);
81
-
82
- // Only single-use links are fulfilled here. A reusable link's "paid" means
83
- // "ever paid"; fulfil those on a payment.confirmed webhook keyed by attempt.id.
84
- if (session.linkType !== "single") return Response.json({ ok: true });
85
-
86
- // Single-use link: fulfil once, keyed by the link itself.
87
- if (session.paid) await fulfilOnce(hint.linkId);
224
+ const result = await genesispay.fulfillment.verify({
225
+ locator: { linkId: hint.linkId },
226
+ expected: expectedSingleUseLinkContract,
227
+ });
228
+ if (result.verified) await fulfilOnce(result.payment.attemptId);
88
229
 
89
230
  return Response.json({ ok: true });
90
231
  }
@@ -96,11 +237,69 @@ Anything else — a missing id, an unknown status like `refunded`, a hand-built
96
237
  — returns `null`. It performs **no verification**: treat it as a prompt to look
97
238
  the link up with your key, never as a receipt.
98
239
 
99
- This pattern fulfils a **single-use** link, keyed by `hint.linkId` (the link's
100
- `publicId`) and refuses any other `linkType`. For a **reusable** link, `paid`
101
- means "ever paid" and the return URL cannot say which payment triggered it —
102
- fulfil those on verified `payment.confirmed` webhooks keyed by `attempt.id` (see
103
- the fulfilment guide below).
240
+ This locator is suitable only for a single-use link. A reusable link can have
241
+ many confirmed attempts, so a link-only locator is ambiguous; use the attempt ID
242
+ from the webhook or recovery response. The browser return remains an untrusted
243
+ UX/recovery hint.
244
+
245
+ ### Payment requests and products
246
+
247
+ `checkout.create()` creates a single-use payment request. Omit `linkType` or
248
+ pass `"single"`; `"reusable"` is rejected locally and by the API (HTTP 422).
249
+ For repeat sales, create a Product and mint its canonical payment link with
250
+ `products.createPaymentLink()`. Each hosted Buy starts a separate checkout
251
+ session. Completed checkouts do not offer a pay-again action. Legacy standalone
252
+ reusable links become single-use requests; previously paid ones are closed,
253
+ while their payments and documents remain available.
254
+
255
+ For a cart or external order, pass an immutable payer-facing `lineItems`
256
+ snapshot. Each `amount` is the total for that line—not a floating-point unit
257
+ price—and every line must add up exactly to the checkout `amount`:
258
+
259
+ ```ts
260
+ await genesispay.checkout.create({
261
+ title: "Order #1042",
262
+ amount: "19.99",
263
+ lineItems: [
264
+ {
265
+ name: "Travel packing cube set",
266
+ description: "Color: Black",
267
+ quantity: 1,
268
+ amount: "15.00",
269
+ sku: "CUBE-BLK",
270
+ imageUrl: "https://shop.example/cube.jpg",
271
+ },
272
+ { name: "Shipping", quantity: 1, amount: "4.99" },
273
+ ],
274
+ }, { idempotencyKey: "order-1042-create" });
275
+ ```
276
+
277
+ WooCommerce may freeze its exact calculated order tax instead of inventing one
278
+ aggregate rate. This is accepted only on authenticated idempotent single-use
279
+ creation; every line needs a `kind`, non-tax lines equal `subtotalMinor`, tax
280
+ lines equal `taxMinor`, and the gross equals `totalMinor`:
281
+
282
+ ```ts
283
+ await genesispay.checkout.create({
284
+ title: "WooCommerce order #1042",
285
+ amount: "71.96",
286
+ taxConfig: {
287
+ version: 2,
288
+ calculation: "external",
289
+ source: "woocommerce",
290
+ subtotalMinor: "59980000",
291
+ taxMinor: "11980000",
292
+ totalMinor: "71960000",
293
+ },
294
+ lineItems: [
295
+ { name: "Products and shipping", quantity: 1, kind: "item", amount: "59.98" },
296
+ { name: "Tax", quantity: 1, kind: "tax", amount: "11.98" },
297
+ ],
298
+ }, { idempotencyKey: "wc-order-1042" });
299
+ ```
300
+
301
+ The external assertion is immutable. GenesisPay verifies its integer identity
302
+ and shows the exact tax but does not determine jurisdiction, rates or filings.
104
303
 
105
304
  ### Amounts and settlement
106
305
 
@@ -109,7 +308,10 @@ amount is therefore a decimal dollar string — never a number:
109
308
 
110
309
  ```ts
111
310
  // 5 USDC / 5 dollars:
112
- await genesispay.checkout.create({ title: "Credits", amount: "5.00" });
311
+ await genesispay.checkout.create(
312
+ { title: "Credits", amount: "5.00" },
313
+ { idempotencyKey: "order-123-create" },
314
+ );
113
315
  ```
114
316
 
115
317
  Buyers can use EUR or USD in the Privy/MoonPay flow; MoonPay converts the local
@@ -145,7 +347,13 @@ export async function POST(request: Request) {
145
347
  request.headers.get("GENESISPAY-SIGNATURE") ?? "",
146
348
  process.env.GENESISPAY_WEBHOOK_SECRET!,
147
349
  );
148
- if (event.type === "payment.confirmed") await fulfil(event.data);
350
+ if (event.type === "payment.confirmed") {
351
+ const verified = await genesispay.fulfillment.verify({
352
+ locator: { attemptId: event.data.attempt.id },
353
+ expected: expectedContractFor(event.data.link.publicId),
354
+ });
355
+ if (verified.verified) await fulfilOnce(verified.payment.attemptId);
356
+ }
149
357
  return new Response(null, { status: 204 });
150
358
  } catch (error) {
151
359
  if (error instanceof GenesisPaySignatureVerificationError) {
@@ -188,28 +396,20 @@ together, since the same hash resolves to nothing on the wrong chain. It is on
188
396
  the *attempt*, not the link, because a pay-by-bank settlement mints on whatever
189
397
  chain the provider uses, which need not be the link's.
190
398
 
191
- A missing `chainId` is **not** the same problem as a missing `asset` — the amount
192
- and currency are still fully determined — but what to do about it depends on you.
193
- If your endpoint only ever receives one chain, book the payment and skip the
194
- explorer link. If it can receive more than one, resolve through
195
- `checkout.retrieve` before booking: an amount whose chain you cannot name is
196
- exactly what this field exists to prevent you from mis-booking.
197
-
198
- Either way, do not default it, and mind the polarity: `undefined !== 8453` is
199
- `true`, so `if (chainId !== 8453)` reads a missing chain as testnet and its
200
- mirror reads it as mainnet. Branch on presence first. And do not assume the set
201
- is Base-only — a pay-by-bank settlement reports the provider's chain, which can
202
- be Gnosis, Polygon or Ethereum.
203
-
204
- **One settlement can reach you twice — handle both cases separately.**
205
-
206
- - *Retries*: a delivery is attempted up to three times on a non-2xx response, and
207
- `event.id` is stable across those attempts. Dedupe on `event.id` before fulfilling.
208
- - *The event pair*: a **single-use** link emits both `payment.confirmed` and
209
- `link.paid` for the same settlement. These are two distinct deliveries with two
210
- **different** `event.id`s, so `event.id` will not collapse them — branch on
211
- `event.type` and fulfil on one of them. Reusable links emit only
212
- `payment.confirmed`.
399
+ A missing or conflicting chain is not fulfillment evidence. Recover the attempt
400
+ through authenticated `fulfillment.verify` against the expected network; do not
401
+ infer the environment, default the chain, or book from tolerant checkout display.
402
+ Strict evidence supports the explicit Base deployment tuple in your contract.
403
+
404
+ **One settlement can reach you through multiple triggers.**
405
+
406
+ - *Retries*: nine sends per cycle, with stable source `event.id` across endpoints,
407
+ retries and audited replay. Persist inbox work before acknowledging.
408
+ - *Event types*: strict payments emit `payment.fulfilled` alongside legacy
409
+ `payment.confirmed`, `link.paid` and product notifications as applicable. Distinct
410
+ event IDs do not collapse one payment across event types or browser/recovery
411
+ triggers. Always use the shared verified-attempt/intent credit transaction
412
+ described in the fulfillment guide below.
213
413
 
214
414
  ### Invoices
215
415
 
@@ -229,9 +429,10 @@ const draft = await genesispay.invoices.create({
229
429
  customerId: customer.publicId,
230
430
  asset: "EURC",
231
431
  dueAt: "2026-08-31T23:59:59.999Z",
232
- taxBps: 2_300, // 23%; manual rate, not automated tax advice
432
+ calculationVersion: 2,
233
433
  lineItems: [
234
- { description: "Consulting", quantity: 2, unitAmount: "450.00" },
434
+ { description: "Consulting", quantity: 2, unitAmount: "450.00",
435
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 2300, note: null } },
235
436
  ],
236
437
  });
237
438
 
@@ -245,12 +446,89 @@ invoice.pdfUrl; // printable PDF
245
446
  invoice.payment.payUrl; // canonical GenesisPay checkout
246
447
  ```
247
448
 
449
+ New invoices use inclusive per-line tax: `unitAmount` includes tax. Each line
450
+ requires `taxConfig`; invoice-wide `taxBps` is zero. Existing v1 drafts keep
451
+ their original exclusive calculation; send `calculationVersion: 1` when editing
452
+ one. To explicitly upgrade a draft, supply `calculationVersion: 2`, `taxBps: 0`
453
+ and a confirmed `taxConfig` for every line. Finalized history cannot be upgraded.
454
+
248
455
  All invoice money fields ending in `Minor` are integer strings. `paid` is a
249
456
  read-only projection of a confirmed, non-simulated GenesisPay payment; it cannot
250
457
  be set through the SDK. Finalized invoices cannot be edited. Use
251
458
  `invoices.void()` to cancel collection or `invoices.markUncollectible()` to write
252
459
  off an unpaid invoice.
253
460
 
461
+ ### Browse invoice summaries
462
+
463
+ `invoices.listSummaries()` is available since 1.3.0. An older installed release
464
+ lacks the method; it can call the authenticated HTTP endpoint directly:
465
+
466
+ ```ts
467
+ const response = await fetch(`${baseUrl}/api/v1/invoices/summaries?limit=25`, {
468
+ headers: { Authorization: `Bearer ${sellerApiKey}` },
469
+ });
470
+ if (!response.ok) throw new Error(`Invoice list failed: ${response.status}`);
471
+ const summaries = await response.json();
472
+ ```
473
+
474
+ Use `invoices.listSummaries()` for bounded pages. The default limit is 25, with
475
+ an integer maximum of 100. Pass the opaque `nextCursor` back as `after`; a null
476
+ cursor means the end. Ordering is newest first by creation time and ID, so
477
+ inserts above your current page do not duplicate older rows. This is a live
478
+ list: status can change between requests.
479
+
480
+ ```ts
481
+ let page = await genesispay.invoices.listSummaries({ limit: 25 });
482
+ for (const invoice of page.invoices) {
483
+ console.log(invoice.publicId, invoice.customer.name, invoice.totalMinor);
484
+ }
485
+ if (page.nextCursor) {
486
+ page = await genesispay.invoices.listSummaries({ limit: 25, after: page.nextCursor });
487
+ }
488
+ if (page.invoices[0]) {
489
+ const fullInvoice = await genesispay.invoices.retrieve(page.invoices[0].publicId);
490
+ }
491
+ ```
492
+
493
+ Each summary has `id`, `publicId`, nullable `invoiceNumber`, `status`, `asset`,
494
+ `chainId`, exact integer-string `totalMinor`, `customer: { name, companyName }`,
495
+ `dueAt`, `createdAt`, and nullable `paidAt`. Finalized customer names come from
496
+ the issued snapshot. A summary intentionally has no line items, delivery
497
+ history, seller profile or payment detail; retrieve an invoice to read those.
498
+ The existing `invoices.list()` still returns its newest 100 full invoices with
499
+ every nested line and delivery. It does not accept pagination options.
500
+
501
+ ### Hosted checkout documents
502
+
503
+ Every new confirmed, non-simulated hosted human checkout creates one immutable
504
+ invoice identity and two PDF artifacts: invoice and payment receipt. New human
505
+ authorizations require complete, attested KYB-approved issuer details and an
506
+ explicit item tax assertion; missing configuration blocks checkout, not silently
507
+ zero tax. Historical v1 enhanced receipts remain readable.
508
+ GenesisPay does not determine the seller's tax rate or require an Avalara or
509
+ Stripe Tax account. This archive is separate
510
+ from seller-authored `invoices`: do not try to create, edit, or number these
511
+ documents through the SDK.
512
+
513
+ ```ts
514
+ const documents = await genesispay.checkoutDocuments.list();
515
+ const oneDocument = await genesispay.checkoutDocuments.get("doc_…");
516
+
517
+ for (const document of documents) {
518
+ console.log(document.kind, document.invoiceNumber, document.totalMinor);
519
+ // document.snapshot is the buyer/seller/tax snapshot frozen before payment.
520
+ }
521
+ ```
522
+
523
+ Amounts ending in `Minor` are integer strings. A `null` `invoiceNumber` means
524
+ the document is an enhanced receipt, not an invoice with a missing number.
525
+ `checkoutDocuments.list()` returns the seller's newest 100 documents.
526
+ The authenticated PDF download is
527
+ `GET /api/v1/checkout-documents/:publicId/pdf` (invoice) and
528
+ `GET /api/v1/checkout-documents/:publicId/pdf?kind=receipt` (payment receipt).
529
+ V2 snapshots include per-line inclusive totals, item treatment, and the reviewed
530
+ seller profile version. Neither document creates another payable invoice.
531
+
254
532
  ### Subscriptions
255
533
 
256
534
  Plans are the reusable template you create once and hand to any number of
@@ -273,10 +551,38 @@ further signatures and emit `subscription.renewed` (or `subscription.past_due`).
273
551
  `genesispay.mandates.*` exposes the per-payer authorizations underneath —
274
552
  `create`, `retrieve`, `charge` (per-use metering) and `revoke`.
275
553
 
554
+ Every per-use charge requires a stable idempotency key. Reuse it after any
555
+ timeout or 503; never generate a second key for the same metered operation:
556
+
557
+ ```ts
558
+ await genesispay.mandates.charge(mandateId, {
559
+ amount: "0.08",
560
+ idempotencyKey: usageRequestId,
561
+ resourceUrl: "https://api.example/forecast",
562
+ });
563
+ ```
564
+
565
+ If the pull was submitted but its receipt is not definitive, the promise rejects
566
+ with `GenesisPayMandateChargePendingError`. Its `status`, `code` and `charge`
567
+ fields preserve the API's 503 locator (including `charge.txHash` when known), so
568
+ the caller can record the unknown outcome and retry only with the same key.
569
+
570
+ `createMandateGate` likewise requires the protected request to carry
571
+ `Idempotency-Key`. An outcome-unknown pull stays 503 with its charge locator and
572
+ the wrapped handler is not opened until the original charge is settled. The
573
+ gate treats HTTP success as transport only: the response must also name a
574
+ settled charge, the exact requested integer minor amount and a full transaction
575
+ hash. A missing, submitted, hashless or contradictory 2xx body fails closed as
576
+ 503 and the same idempotency key remains authoritative.
577
+
276
578
  There is deliberately **no `mandates.activate`**: activation requires the payer's
277
579
  own signature over the permit, so it belongs in the payer's frontend, not in a
278
580
  server holding your secret key.
279
581
 
582
+ `MandateStatus` also includes terminal `cancelled`: GenesisPay stopped an
583
+ unbroadcast permit because seller eligibility changed. That authority never
584
+ wakes after recovery; create a new proposal and obtain a fresh payer signature.
585
+
280
586
  **Cancelling a subscription** — list the plan's subscribers, find the payer, revoke:
281
587
 
282
588
  ```ts
@@ -314,7 +620,8 @@ and you get the same link with `created: false`.
314
620
  ```ts
315
621
  const product = await genesispay.products.create({
316
622
  name: "Market data report",
317
- price: "2.00",
623
+ price: "2.00", // inclusive customer total
624
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 2000, note: null },
318
625
  sku: "MDR-1",
319
626
  // A redirect product creates a signed, expiring entitlement on every sale.
320
627
  delivery: {
@@ -330,6 +637,63 @@ const { link, created } = await genesispay.products.createPaymentLink(
330
637
  // Share link.payUrl — every sale of this product settles through it.
331
638
  ```
332
639
 
640
+ #### Item tax and existing catalogue entries
641
+
642
+ `taxConfig` is `{ version: 1, treatment, rateBps, note }`. `rateBps` is an integer
643
+ (2000 = 20%), from 1 to 10000 for `taxable`. `zero_rated`, `exempt`, and
644
+ `not_collected` require rate 0 and a nonempty seller-provided explanation in
645
+ `note`. There is no automatic reverse charge or country-based tax determination.
646
+ Omitted tax on legacy API product/link creation remains **unconfigured**, not 0%;
647
+ new human checkout cannot start until the item and verified seller are ready.
648
+ Agent/x402 behavior remains separate. `checkout.create` accepts the same
649
+ version-1 `taxConfig`; strict WooCommerce order creation may instead use the
650
+ version-2 exact external assertion shown above.
651
+
652
+ Publish a tax correction using the exact configuration you last read:
653
+
654
+ ```ts
655
+ await genesispay.products.update(product.publicId, {
656
+ expectedTaxConfig: product.taxConfig, // null for an unconfigured product
657
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 1000, note: null },
658
+ });
659
+ // Standalone links only; product-backed links are changed through products.update:
660
+ await genesispay.checkout.updateTax(link.publicId, {
661
+ expectedTaxConfig: link.taxConfig,
662
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 1000, note: null },
663
+ });
664
+ ```
665
+
666
+ The server atomically publishes product tax to its live link without repricing
667
+ it. A concurrent edit returns 409: re-read and review before retrying. Existing
668
+ payment snapshots and issued documents never change. Archived and manual-invoice
669
+ links reject generic tax updates.
670
+
671
+ For an immutable per-order checkout, assert the complete current product
672
+ contract and create an explicitly identified single-use link. Both calls are
673
+ versioned; creation requires seller-account-scoped idempotency, so API-key
674
+ rotation does not break a retry:
675
+
676
+ ```ts
677
+ await genesispay.products.assertContract(expectedProductContract);
678
+
679
+ const checkout = await genesispay.products.createCheckout(
680
+ {
681
+ expected: expectedProductContract,
682
+ clientReferenceId: order.id,
683
+ metadata: { buyerId: order.buyerId },
684
+ returnUrl: "https://shop.example.com/thanks",
685
+ },
686
+ { idempotencyKey: `product-checkout-${order.id}` },
687
+ );
688
+
689
+ checkout.linkId; // GenesisPay link identity — there is no ambiguous checkout.id
690
+ checkout.payUrl;
691
+ ```
692
+
693
+ The checkout copies the product's tax configuration inside the same transaction
694
+ that freezes its price and delivery contract. Tax never changes the signed gross
695
+ amount or serves as fulfilment authority.
696
+
333
697
  #### Never hardcode `link.payUrl`
334
698
 
335
699
  The URL embeds the link's `inv_…` id, and that id is **per link, not per
@@ -357,22 +721,20 @@ A confirmed purchase fires the `product.purchased` webhook. Its payload
357
721
  carries the buyer's entitlement — `entitlement.redemptionPath` is a signed,
358
722
  expiring redirect (~30 days) to your fulfilment URL, with `gp_*` parameters
359
723
  (`gp_entitlement`, `gp_attempt`, `gp_simulated`, …) you can verify server-side
360
- via `GET /api/v1/entitlements/verify`. Always check `simulated`: test-mode
361
- purchases deliver end to end, marked, so your handler must not book them as
362
- revenue.
724
+ via the tolerant entitlement view for migration and display. Those parameters,
725
+ the webhook object, and `entitlements.verify` are not fulfilment authority in
726
+ 1.0. Test-mode purchases still deliver end to end with an explicit simulation
727
+ marker, but only the strict verifier can return an authority-bearing success.
363
728
 
364
- Use the SDK rather than trusting `gp_*` query parameters from the browser:
729
+ Use the strict authority verifier rather than trusting `gp_*` query parameters
730
+ or the tolerant entitlement view:
365
731
 
366
732
  ```ts
367
- const verified = await genesispay.entitlements.verify(entitlementId);
368
- if (
369
- !verified.valid ||
370
- verified.entitlement.simulated ||
371
- verified.entitlement.productId !== product.publicId
372
- ) {
373
- throw new Error("invalid entitlement");
374
- }
375
- // Insert verified.entitlement.publicId under a unique constraint, then fulfil.
733
+ const verified = await genesispay.fulfillment.verify({
734
+ locator: { entitlementId },
735
+ expected: expectedProductContract,
736
+ });
737
+ if (verified.verified) await fulfilOnce(verified.payment.attemptId);
376
738
  ```
377
739
 
378
740
  ### Product-backed API gate
@@ -400,13 +762,26 @@ Protect the route with that product. Validate request shape before calling
400
762
  `protect`, and make handler effects idempotent by `purchase.payment.attemptId`:
401
763
 
402
764
  ```ts
765
+ import { createGateRequestFingerprint } from "@genesis-tech/genesispay-seller";
766
+
403
767
  const forecastGate = genesispay.products.gate("prod_...");
404
768
 
405
769
  export async function POST(request: Request) {
406
770
  const rawBody = await request.clone().text();
407
771
  validateForecastJson(rawBody); // invalid requests never create a payment attempt
408
772
 
409
- return forecastGate.protect(request, async (_request, purchase) => {
773
+ const expectedForRequest = {
774
+ ...expectedForecastContract,
775
+ delivery: {
776
+ ...expectedForecastContract.delivery,
777
+ gate: {
778
+ ...expectedForecastContract.delivery.gate,
779
+ fingerprint: await createGateRequestFingerprint(request),
780
+ },
781
+ },
782
+ };
783
+
784
+ return forecastGate.protect(request, expectedForRequest, async (_request, purchase) => {
410
785
  const cached = await readForecast(purchase.payment.attemptId);
411
786
  if (cached) return Response.json(cached);
412
787
 
@@ -417,19 +792,95 @@ export async function POST(request: Request) {
417
792
  }
418
793
  ```
419
794
 
420
- On the initial request, `protect` returns the standard `402 Payment Required`.
795
+ `protect` validates the method, canonical resource URL, and request fingerprint
796
+ against the expected immutable contract before negotiation. Its challenge sends
797
+ that complete contract to GenesisPay, which compares it with the exact frozen
798
+ payable link before creating an attempt or returning a `402 Payment Required`;
799
+ the SDK never reconstructs authority from mutable catalogue presentation.
421
800
  GenesisPay creates a pending attempt before that response and advertises a
422
801
  reserved `gp_attempt` value in the x402 resource URL. On the signed retry, the
423
802
  SDK sends only the request fingerprint to GenesisPay; it never sends forecast
424
- inputs. A confirmed replay returns the same payment receipt with
425
- `idempotentReplay: true`; handlers may therefore execute more than once and
426
- must keep their own result cache.
803
+ inputs. After settlement it retrieves strict evidence by attempt ID and invokes
804
+ the handler only after every authority field matches. If evidence is unavailable
805
+ it returns a recoverable `503` carrying `GENESISPAY-Payment-Attempt-Id` and
806
+ preserves a matching `PAYMENT-RESPONSE`. A transient response is `retryable: true` and
807
+ carries `Retry-After: 2`; a permanent inconsistency is `retryable: false` and
808
+ deliberately carries no retry instruction. The gate transport also negotiates
809
+ `GENESISPAY-Version: 2026-08-26`, while unversioned 0.x gate traffic keeps its
810
+ legacy wire contract. If the settlement connection drops after commit, the SDK
811
+ recovers the untrusted attempt locator from the signed x402 payload and returns
812
+ the same retryable 503; only `fulfillment.verify` can turn that locator into
813
+ authority. Handlers may execute more than once and must keep their own result
814
+ cache.
815
+
816
+ An interrupted verification response stream is transient; a completed malformed
817
+ evidence response is permanent. A settlement naming a different attempt drops
818
+ that unrelated receipt and reports `settlement_outcome_unknown` with the signed
819
+ attempt locator. Missing strict settlement metadata or a negative
820
+ `not_found`/`not_confirmed` verification likewise cannot claim confirmation.
821
+ Only independently verified evidence allows the handler to run.
822
+
823
+ You may retry the original signed request after its authorization expires if
824
+ that attempt was already confirmed. GenesisPay verifies the persisted attempt,
825
+ signature and original on-chain payment before replaying success; it does not
826
+ broadcast or collect a fee again. An unpaid expired authorization remains
827
+ rejected. Keep the same attempt locator and signature when recovering a payment.
828
+ Failed/expired attempts are terminal even if their signature is still valid or
829
+ the original link has been archived (`409 attempt_failed` / `attempt_expired`).
427
830
 
428
831
  `products.list({ includeArchived: true })`, `products.retrieve`,
429
832
  `products.archive` complete the namespace. Archiving stops **new** link mints;
430
833
  the existing canonical link stays payable. The price is copied onto the link
431
834
  at mint — a later catalogue edit never changes what a buyer already sees.
432
835
 
836
+ #### Prepare requests for body-priced resources
837
+
838
+ Before an agent signs, it asks your resource to freeze the exact settlement
839
+ plan: a request carrying `GENESISPAY-Settlement-Prepare: 1`. A client that
840
+ puts its plan parameters in a second header,
841
+ `GENESISPAY-Settlement-Prepare-Params` (base64 JSON
842
+ `{ payer, idempotencyKey, feeMode, authority? }`, decoded by
843
+ `decodeSettlementPrepareParamsHeader` from `@genesis-tech/genesispay-protocol`),
844
+ sends that preparation as a **repeat of the purchase request** — same method,
845
+ URL (plus the reserved `gp_attempt` query), `content-type` and body.
846
+
847
+ **Rollout:** the GenesisPay agent engine does not send the params header yet;
848
+ that is a server follow-up. Until it ships, agents send the legacy form
849
+ described below, so a body-validated route must still accept the legacy
850
+ preparation body — or hand any request carrying
851
+ `GENESISPAY-Settlement-Prepare: 1` to `protect` before its own validation.
852
+
853
+ For a client that sends the header, your route needs no special case. Parse and
854
+ validate the body as for any purchase — read it from `request.clone()` (or pass
855
+ a re-created `Request`) so the gate can still read the body and recompute the
856
+ fingerprint; a consumed body answers `422 { code: "invalid_request" }` — select
857
+ the gate for it (for example the generic gate for a
858
+ request tier, or the product whose registered resource serves that tier) and
859
+ call `protect` or the wrapped handler. A body-priced API needs one gate
860
+ resource per price: a product gate is unique per method and resource URL, so
861
+ each tier is its own registered resource (or its own generic gate). The gate
862
+ recognises the preparation by its header, never runs your handler for it, and
863
+ answers with the plan:
864
+
865
+ - **Product gates** recompute the method, canonical resource URL and request
866
+ fingerprint and require them to equal your expected gate intent, exactly as
867
+ for the challenge and the signed retry. A preparation for a different request
868
+ answers `422 { code: "contract_mismatch", mismatches }` and nothing reaches
869
+ GenesisPay. The forwarded `prepare` action is unchanged.
870
+ - **The generic gate** (`createPaymentGate`) prices by configuration and binds
871
+ no request fingerprint; it reads the parameters from the header and treats
872
+ the body as opaque purchase content.
873
+ - A present but malformed params header answers
874
+ `422 { code: "invalid_request" }`; the gate never falls back to the body.
875
+
876
+ Without the params header, both gates keep the legacy form, in which the body
877
+ carries the plan parameters. It stays supported for GET resources, hosted
878
+ payment links and the current agent engine — but a route that rejects any body
879
+ other than its own purchase schema will refuse it, so such a route must
880
+ recognise the prepare header before its own validation until clients send the
881
+ header form. The decision is
882
+ recorded in ADR-0083 of the GenesisPay repository.
883
+
433
884
  ### Testing the paid path
434
885
 
435
886
  ```ts
@@ -439,8 +890,9 @@ const session = await genesispay.checkout.simulatePayment(publicId);
439
890
 
440
891
  Test keys only — a `gp_sk_live_…` key gets a 403, and on a mainnet deployment the
441
892
  endpoint does not exist at all. The resulting attempt has `txHash: null` (no
442
- transaction happened) and `simulated: true`; branch on that flag rather than on
443
- the missing hash, because a *pending real* attempt has no hash either.
893
+ transaction happened) and `simulated: true` in tolerant display responses. Never
894
+ use that response to fulfil: strict `fulfillment.verify` returns the normal
895
+ negative reason `simulated`, while a pending real attempt can also have no hash.
444
896
 
445
897
  ### Fulfilment guide
446
898
 
@@ -452,15 +904,77 @@ handler run never double-delivers.
452
904
 
453
905
  | Product | Fulfil on | Deduplicate by |
454
906
  |---|---|---|
455
- | Standalone single-use checkout | a verified `payment.confirmed` webhook, or an authenticated `checkout.retrieve()` | the link `publicId` (fulfil once per link) |
456
- | Standalone reusable checkout | each verified `payment.confirmed` attempt | `attempt.id` — never the link-level `paid`, which means "ever paid" |
457
- | Redirect product | a verified, non-simulated entitlement (see Products) | the entitlement `publicId` |
458
- | Product-backed gate | the confirmed purchase inside `protect` | `purchase.payment.attemptId` |
907
+ | Standalone single-use checkout | strict `fulfillment.verify` by link or attempt | confirmed `payment.attemptId` |
908
+ | Product without digital delivery | strict `fulfillment.verify` by attempt | `payment.attemptId` — never link-level `paid` |
909
+ | Redirect product | strict `fulfillment.verify` by entitlement or attempt | `payment.attemptId` |
910
+ | Product-backed gate | strict evidence returned after confirmed settlement | `payment.attemptId` |
911
+
912
+ `payment.fulfilled` is the canonical notification. Its `data` is the strict
913
+ FulfillmentEvidence wire shape plus explicit nullable `clientReferenceId`; its
914
+ envelope has `apiVersion: "2026-08-26"` and `livemode`. Simulation does not emit
915
+ it. `constructEvent` verifies raw bytes before parsing and validates known evidence
916
+ fields, preserving integer amount strings. It does **not** return VerifiedPayment:
917
+ always call authenticated `fulfillment.verify({ locator, expected })` afterward.
918
+ The name does not claim that your application has delivered anything.
919
+
920
+ `livemode` is not present on every event type. Do not discard an event because
921
+ `!event.livemode` is true: an absent field also passes that check. For
922
+ `payment.fulfilled`, `livemode: false` means Base Sepolia, and
923
+ `data.simulation.simulated` is explicitly false. Network and simulation are
924
+ separate facts; never substitute `livemode` for the event's simulation field.
925
+ Use authenticated `fulfillment.verify` before granting credits or fulfilling
926
+ an order.
927
+
928
+ Persist a purchase intent (account, credits, expected contract) before checkout.
929
+ Use its ID as `clientReferenceId` and the checkout idempotency key. Store the
930
+ returned link ID and require `verified.payment.linkId` to match it before credit.
931
+ If the webhook precedes that response, keep it pending and recover the same
932
+ checkout; never assign a payment to an account from webhook metadata alone.
933
+
934
+ New event IDs are stable across endpoints, retries and explicit replay. Dedupe
935
+ inbox processing by event ID, but **atomically claim verified attempt ID and
936
+ purchase intent with the credit balance/ledger update** across every trigger.
937
+ Different event types, browser return and reconciliation must converge there.
938
+ Keep a conflicting binding for investigation; never grant a second credit.
939
+ Persist the inbox before acknowledging 2xx and let a worker retry verification.
940
+
941
+ There are nine sends per delivery cycle: initial plus 30s, 2m, 5m, 15m, 1h, 6h,
942
+ 12h, 24h delays (±10% jitter), maximum72h. Minute scheduling rounds due work up to
943
+ the next tick. Only explicit audited operator replay opens another cycle, with
944
+ identical body and event ID. Acknowledgement loss permits duplicates. Outbox
945
+ producers/worker must be deployed and measured before relying on these targets.
946
+
947
+ ### Recover missing notifications
459
948
 
460
- Webhook deliveries are retried (up to three attempts, stable `event.id`), and a
461
- single-use link emits both `payment.confirmed` and `link.paid` for one settlement —
462
- dedupe on `event.id`, and branch on `event.type` so you fulfil once (see
463
- Webhooks above).
949
+ ```ts
950
+ let cursor: string | undefined;
951
+ do {
952
+ const page = await genesispay.fulfillment.listAttempts({
953
+ clientReferenceId: intent.id,
954
+ confirmedAfter: intent.createdAt,
955
+ limit: 50,
956
+ cursor,
957
+ });
958
+ for (const candidate of page.data) {
959
+ const verified = await genesispay.fulfillment.verify({
960
+ locator: { attemptId: candidate.attemptId }, expected: intent.expected,
961
+ });
962
+ if (verified.verified && verified.payment.linkId === intent.linkId) {
963
+ // Your shared transaction claims attempt + intent and writes credits.
964
+ await creditVerifiedPurchaseOnce(intent, verified.payment);
965
+ }
966
+ }
967
+ cursor = page.nextCursor ?? undefined;
968
+ } while (cursor);
969
+ ```
970
+
971
+ Listing requires the strict API version and seller key and enforces seller/mode
972
+ scope. Optional exact reference (max200), inclusive ISO confirmedAfter/Before,
973
+ limit1..100(default50) and opaque cursor are supported. Keep filters unchanged
974
+ between pages. A cursor fixes the upper bound and preserves database microseconds.
975
+ Repeat scans for late commits; include abandoned, expired and cancelled browser
976
+ flows. `authorityVersion: null` is a historical diagnostic, never permission to
977
+ credit. No listing row is authority, even when it names a confirmed attempt.
464
978
 
465
979
  ### Correlation, and the `cs` query parameter
466
980
 
@@ -492,18 +1006,27 @@ The `returnUrl`/`cancelUrl` limits and the amount-conflict check are validated
492
1006
  remaining limits (`metadata`, `clientReferenceId`) are enforced server-side and
493
1007
  fail the `checkout.create` call with a 422.
494
1008
 
495
- ### Note on `paid` for reusable links
1009
+ ### Note on `paid` for product links
496
1010
 
497
1011
  `session.paid` is derived from `confirmedPaymentCount > 0`. For a `reusable` link
498
1012
  that counter only ever grows, so `paid` stays `true` from the first payment
499
1013
  onward — it answers "has this link ever been paid", not "has *this* buyer paid".
500
- For per-buyer fulfilment on a reusable link, use a webhook, or track
501
- `confirmedPaymentCount` as a delta.
1014
+ For per-purchase fulfilment on a product link, use a webhook as a trigger and then
1015
+ strictly verify its attempt ID. Never use `confirmedPaymentCount` as authority.
502
1016
 
503
1017
  Options: `baseUrl` (override the mode default — https, http only for localhost;
504
1018
  both modes default to the GenesisPay facilitator, so it is optional), `configTtlMs`
505
1019
  (seller-config cache TTL, default 5 min), `expectedPayTo` (recommended for `live`
506
1020
  keys — a local pin that fail-closes if the resolved wallet ever differs), `fetchFn`.
1021
+ The pin also applies to `checkout.retrieve`, `products.assertContract`,
1022
+ `products.createCheckout`, `products.gate(...).protect`, and `fulfillment.verify`.
1023
+ Checkout retrieval checks the raw destination before its display mapper runs;
1024
+ strict methods refuse a conflicting expected destination before a network request.
1025
+ Missing or conflicting destinations fail closed. An explicit expected contract
1026
+ cannot override the client pin, including on historical verification.
1027
+ After a wallet rotation, reconcile old payments with a separate client pinned
1028
+ to the original destination recorded in your immutable order contract. Keep the
1029
+ current client pinned to the new wallet; neither pin rewrites payment history.
507
1030
  The client fails **closed**: an unreachable backend returns `503` and a seller with
508
1031
  no wallet returns a `402` `payment_not_configured` — the paid handler never runs
509
1032
  without a valid destination. It also refuses to advertise a wallet whose network
@@ -596,7 +1119,10 @@ The same wrapped handler drops straight into `Bun.serve({ fetch: gated })`.
596
1119
 
597
1120
  `gate.wrap(handler, { verifySettlement })` requires a settlement hook. Use the
598
1121
  built-in `genesisPaySettlement({ facilitatorBaseUrl, apiKey })`, which POSTs the
599
- payment to GenesisPay's facilitator (`/api/v1/facilitator/settle`). GenesisPay
1122
+ plan-aware prepare request before an agent signs, then forwards the signed
1123
+ authorization with its immutable settlement-plan id. Clients that omit the
1124
+ prepare handshake are refused before broadcast with `settlement_plan_required`.
1125
+ The hook sends preparation and settlement to GenesisPay's facilitator. GenesisPay
600
1126
  broadcasts the EIP-3009 authorization on-chain, verifies the USDC transfer,
601
1127
  and returns the receipt. `facilitatorBaseUrl` is optional and defaults to the
602
1128
  public development facilitator (`DEFAULT_FACILITATOR_BASE_URL`,
@@ -630,3 +1156,105 @@ const verifySettlement: VerifySettlement = async ({ payment, requirement }) => {
630
1156
  your `verifySettlement` hook.
631
1157
  - Get a seller API key (`gp_sk_...`) from your GenesisPay dashboard under
632
1158
  Developers.
1159
+
1160
+ ### Beta: new mandate authority answers 409
1161
+
1162
+ During the GenesisPay beta, a deployment may lock the creation of **new** mandate
1163
+ authority. While it is locked, `mandates.create`, the `contract_mandate_v1`
1164
+ helpers (`mandates.createContractSubscription`, `mandates.createContractPerUse`,
1165
+ `mandates.proposeContractUsage`) and `plans.create` reject with HTTP `409` and
1166
+ code `mandate_authority_locked_for_beta`. There is no `Retry-After` — waiting
1167
+ does not change the answer, so treat it as a capability that is off rather than a
1168
+ transient failure.
1169
+
1170
+ `mandates.submitContractUsage`, `mandates.getContractUsage`, revocation, every
1171
+ list/read, and the whole x402 payment-gate path are unaffected, as is any mandate
1172
+ a payer already approved. A retry of a request made before the lock still returns
1173
+ its original charge, mandate or payment identity — keep using the SAME
1174
+ idempotency key, exactly as you would without the lock. In particular a
1175
+ `mandates.charge` retry whose key already names a charge still comes back with
1176
+ that charge (settled, or `submitted` with its locator) rather than the 409, so a
1177
+ `409 mandate_authority_locked_for_beta` always means no charge exists for that
1178
+ key. Never retry with a fresh key to work around it.
1179
+
1180
+ ### Experimental gated contract subscriptions
1181
+
1182
+ `mandates.createContractSubscription` explicitly requests the two-signature
1183
+ `contract_mandate_v1` protocol. It requires an ISO expiry and validates both
1184
+ returned signing payloads. This capability is off by default; the reviewed
1185
+ contract registry must permit the deployment. Mainnet also requires legal/audit
1186
+ and completed legacy-authority cutover evidence.
1187
+
1188
+ ```ts
1189
+ const proposal = await genesispay.mandates.createContractSubscription({
1190
+ payerWallet, allowance: "108", capPerCharge: "9", amountPerPeriod: "9",
1191
+ periodDays: 30, validUntil: "2027-09-01T00:00:00Z",
1192
+ });
1193
+ // In the payer frontend, sign approval.termsTypedData, then approval.permitTypedData.
1194
+ // POST /api/v1/mandates/:id/activate:
1195
+ // { protocol: "contract_mandate_v1", termsSignature, permitSignature }
1196
+ ```
1197
+
1198
+ HTTP202 is acceptance, not activation or payment confirmation. Retry the same
1199
+ signatures after an unknown HTTP outcome. A409 `mandate_proposal_stale` requires a
1200
+ new proposal and two new approvals. The legacy `mandates.create` method keeps its
1201
+ one-permit contract and cannot silently switch protocols.
1202
+
1203
+ `mandates.revoke(id)` requests local cancellation for contract mandates; inspect
1204
+ `revocationStatus`. The payer signs `revocationTypedData` from the returned API
1205
+ mandate and any relay can POST `{ protocol: "contract_mandate_v1", signature }`
1206
+ to `/api/v1/mandates/:id/revoke-signed`. Only `confirmed` means the permanent
1207
+ on-chain revocation exists. An independent relay may instead call the bound
1208
+ executor's `revokeWithSignature(payer, mandateId, signature)`; the payer can call
1209
+ `revoke(mandateId)` directly. Neither path requires an API session. Do not use
1210
+ `approve(0)` as proof that old signed permits are invalid.
1211
+
1212
+ Contract subscription renewals are admitted by the worker or cron and become paid
1213
+ only after canonical atomic receipt verification. First charge is eligible at
1214
+ activation; later charges wait the signed interval after successful settlement.
1215
+ Downtime does not create catch-up charges. A confirmed revert may receive a new
1216
+ attempt only after exact receipt and unused-authority verification.
1217
+
1218
+ Contract billing webhooks add `protocol: "contract_mandate_v1"`, `contractEvent`
1219
+ (immutable chain/block/log, logical identity, sequence and split amounts), and
1220
+ `mandateStateContext: { kind: "projection_snapshot", observedAt }`. `occurredAt`
1221
+ is the event's block time; `mandate` is the state when that event was projected,
1222
+ which can be later. Deliveries may arrive out of order: an old `mandate.active`
1223
+ event can carry an already revoked snapshot. Order event facts by block/log and
1224
+ retrieve current state before enabling access. Deduplicate the envelope `id`;
1225
+ replays preserve its original bytes, including the original snapshot.
1226
+
1227
+ ### Experimental gated signed per-use mandates
1228
+
1229
+ `mandates.createContractPerUse({ payerWallet, allowance, capPerCharge, validUntil })`
1230
+ returns the same two bounded approvals as the contract subscription API. It has
1231
+ no recurring amount or interval. After the payer signs both approvals and the
1232
+ activation confirms, each usage request requires its own payer signature:
1233
+
1234
+ ```ts
1235
+ const charge = await genesispay.mandates.proposeContractUsage({
1236
+ mandate: approvedMandate, // the saved createContractPerUse result
1237
+ amountMinor: "80000", // gross integer minor units, including any fee
1238
+ idempotencyKey: "forecast-request-001",
1239
+ resourceUrl: "https://example.com/forecast",
1240
+ });
1241
+ // The payer's wallet signs charge.chargeTypedData. The seller SDK never signs.
1242
+ const accepted = await genesispay.mandates.submitContractUsage(charge, payerSignature);
1243
+ const current = await genesispay.mandates.getContractUsage(accepted);
1244
+ // Fulfill only after current.status === "settled", verified against your order.
1245
+ ```
1246
+
1247
+ The SDK independently builds the charge EIP-712 message from the saved mandate
1248
+ consent and exact requested gross. It checks chain, executor, mandate, fee,
1249
+ request metadata and signing fields. Submission and polling preserve the original
1250
+ charge identity and deadline. HTTP202 means pending; queue acceptance is not a
1251
+ confirmed payment. A retry uses the same idempotency key and saved proposal.
1252
+ Changing a request under that key returns a conflict, including after failure.
1253
+
1254
+ The server retains the proposal before a wallet prompt. If no signature arrives,
1255
+ it closes the source only after finalized proof that the charge is unused and
1256
+ its authority expired or was revoked. The old `mandates.charge` and metering gate
1257
+ are legacy-only and refuse contract mandates; they cannot silently replace the
1258
+ required signature. These contract APIs remain gated and unreleased on Mainnet.
1259
+ Agent policy/signing integration and per-use successors after a mined revert
1260
+ remain separate pending implementation work.