@genesis-tech/genesispay-seller 0.13.1 → 1.3.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 (99) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +670 -91
  3. package/dist/checkout-documents.d.ts +99 -0
  4. package/dist/checkout-documents.d.ts.map +1 -0
  5. package/dist/checkout-documents.js +155 -0
  6. package/dist/checkout-documents.js.map +1 -0
  7. package/dist/checkout-return.d.ts +41 -0
  8. package/dist/checkout-return.d.ts.map +1 -0
  9. package/dist/checkout-return.js +68 -0
  10. package/dist/checkout-return.js.map +1 -0
  11. package/dist/client.d.ts +318 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +718 -0
  14. package/dist/client.js.map +1 -0
  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/customers.d.ts +41 -0
  24. package/dist/customers.d.ts.map +1 -0
  25. package/dist/customers.js +76 -0
  26. package/dist/customers.js.map +1 -0
  27. package/dist/entitlements.d.ts +19 -0
  28. package/dist/entitlements.d.ts.map +1 -0
  29. package/dist/entitlements.js +30 -0
  30. package/dist/entitlements.js.map +1 -0
  31. package/dist/errors.d.ts +129 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +273 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/fulfillment-attempts.d.ts +29 -0
  36. package/dist/fulfillment-attempts.d.ts.map +1 -0
  37. package/dist/fulfillment-attempts.js +71 -0
  38. package/dist/fulfillment-attempts.js.map +1 -0
  39. package/dist/fulfillment.d.ts +152 -0
  40. package/dist/fulfillment.d.ts.map +1 -0
  41. package/dist/fulfillment.js +810 -0
  42. package/dist/fulfillment.js.map +1 -0
  43. package/dist/genesispay-settlement.d.ts +32 -0
  44. package/dist/genesispay-settlement.d.ts.map +1 -0
  45. package/dist/genesispay-settlement.js +500 -0
  46. package/dist/genesispay-settlement.js.map +1 -0
  47. package/dist/index.d.ts +22 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +21 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/invoices.d.ts +111 -0
  52. package/dist/invoices.d.ts.map +1 -0
  53. package/dist/invoices.js +192 -0
  54. package/dist/invoices.js.map +1 -0
  55. package/dist/item-tax.d.ts +15 -0
  56. package/dist/item-tax.d.ts.map +1 -0
  57. package/dist/item-tax.js +19 -0
  58. package/dist/item-tax.js.map +1 -0
  59. package/dist/mandate-gate.d.ts +54 -0
  60. package/dist/mandate-gate.d.ts.map +1 -0
  61. package/dist/mandate-gate.js +183 -0
  62. package/dist/mandate-gate.js.map +1 -0
  63. package/dist/mandates.d.ts +189 -0
  64. package/dist/mandates.d.ts.map +1 -0
  65. package/dist/mandates.js +234 -0
  66. package/dist/mandates.js.map +1 -0
  67. package/dist/networks.d.ts +10 -0
  68. package/dist/networks.d.ts.map +1 -0
  69. package/dist/networks.js +23 -0
  70. package/dist/networks.js.map +1 -0
  71. package/dist/payment-gate.d.ts +98 -0
  72. package/dist/payment-gate.d.ts.map +1 -0
  73. package/dist/payment-gate.js +370 -0
  74. package/dist/payment-gate.js.map +1 -0
  75. package/dist/plans.d.ts +68 -0
  76. package/dist/plans.d.ts.map +1 -0
  77. package/dist/plans.js +127 -0
  78. package/dist/plans.js.map +1 -0
  79. package/dist/products.d.ts +170 -0
  80. package/dist/products.d.ts.map +1 -0
  81. package/dist/products.js +1317 -0
  82. package/dist/products.js.map +1 -0
  83. package/dist/resource.d.ts +47 -0
  84. package/dist/resource.d.ts.map +1 -0
  85. package/dist/resource.js +71 -0
  86. package/dist/resource.js.map +1 -0
  87. package/dist/strict-response.d.ts +21 -0
  88. package/dist/strict-response.d.ts.map +1 -0
  89. package/dist/strict-response.js +68 -0
  90. package/dist/strict-response.js.map +1 -0
  91. package/dist/usdc-amount.d.ts +7 -0
  92. package/dist/usdc-amount.d.ts.map +1 -0
  93. package/dist/usdc-amount.js +21 -0
  94. package/dist/usdc-amount.js.map +1 -0
  95. package/dist/webhooks.d.ts +352 -0
  96. package/dist/webhooks.d.ts.map +1 -0
  97. package/dist/webhooks.js +244 -0
  98. package/dist/webhooks.js.map +1 -0
  99. package/package.json +4 -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 targets the coordinated **1.3.0 release candidate**. npm currently
50
+ serves 0.13.2. The candidate requires protocol 0.5.0 and the matching backend;
51
+ install it only after those versions are published and the deployment is ready.
52
+
20
53
  ```bash
21
- npm install @genesis-tech/genesispay-seller
54
+ npm install @genesis-tech/genesispay-seller@1.3.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,21 @@ 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. No npm publication is
254
+ implied by this repository change.
104
255
 
105
256
  ### Amounts and settlement
106
257
 
@@ -109,7 +260,10 @@ amount is therefore a decimal dollar string — never a number:
109
260
 
110
261
  ```ts
111
262
  // 5 USDC / 5 dollars:
112
- await genesispay.checkout.create({ title: "Credits", amount: "5.00" });
263
+ await genesispay.checkout.create(
264
+ { title: "Credits", amount: "5.00" },
265
+ { idempotencyKey: "order-123-create" },
266
+ );
113
267
  ```
114
268
 
115
269
  Buyers can use EUR or USD in the Privy/MoonPay flow; MoonPay converts the local
@@ -145,7 +299,13 @@ export async function POST(request: Request) {
145
299
  request.headers.get("GENESISPAY-SIGNATURE") ?? "",
146
300
  process.env.GENESISPAY_WEBHOOK_SECRET!,
147
301
  );
148
- if (event.type === "payment.confirmed") await fulfil(event.data);
302
+ if (event.type === "payment.confirmed") {
303
+ const verified = await genesispay.fulfillment.verify({
304
+ locator: { attemptId: event.data.attempt.id },
305
+ expected: expectedContractFor(event.data.link.publicId),
306
+ });
307
+ if (verified.verified) await fulfilOnce(verified.payment.attemptId);
308
+ }
149
309
  return new Response(null, { status: 204 });
150
310
  } catch (error) {
151
311
  if (error instanceof GenesisPaySignatureVerificationError) {
@@ -188,28 +348,20 @@ together, since the same hash resolves to nothing on the wrong chain. It is on
188
348
  the *attempt*, not the link, because a pay-by-bank settlement mints on whatever
189
349
  chain the provider uses, which need not be the link's.
190
350
 
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`.
351
+ A missing or conflicting chain is not fulfillment evidence. Recover the attempt
352
+ through authenticated `fulfillment.verify` against the expected network; do not
353
+ infer the environment, default the chain, or book from tolerant checkout display.
354
+ Strict evidence supports the explicit Base deployment tuple in your contract.
355
+
356
+ **One settlement can reach you through multiple triggers.**
357
+
358
+ - *Retries*: nine sends per cycle, with stable source `event.id` across endpoints,
359
+ retries and audited replay. Persist inbox work before acknowledging.
360
+ - *Event types*: strict payments emit `payment.fulfilled` alongside legacy
361
+ `payment.confirmed`, `link.paid` and product notifications as applicable. Distinct
362
+ event IDs do not collapse one payment across event types or browser/recovery
363
+ triggers. Always use the shared verified-attempt/intent credit transaction
364
+ described in the fulfillment guide below.
213
365
 
214
366
  ### Invoices
215
367
 
@@ -229,9 +381,10 @@ const draft = await genesispay.invoices.create({
229
381
  customerId: customer.publicId,
230
382
  asset: "EURC",
231
383
  dueAt: "2026-08-31T23:59:59.999Z",
232
- taxBps: 2_300, // 23%; manual rate, not automated tax advice
384
+ calculationVersion: 2,
233
385
  lineItems: [
234
- { description: "Consulting", quantity: 2, unitAmount: "450.00" },
386
+ { description: "Consulting", quantity: 2, unitAmount: "450.00",
387
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 2300, note: null } },
235
388
  ],
236
389
  });
237
390
 
@@ -245,12 +398,90 @@ invoice.pdfUrl; // printable PDF
245
398
  invoice.payment.payUrl; // canonical GenesisPay checkout
246
399
  ```
247
400
 
401
+ New invoices use inclusive per-line tax: `unitAmount` includes tax. Each line
402
+ requires `taxConfig`; invoice-wide `taxBps` is zero. Existing v1 drafts keep
403
+ their original exclusive calculation; send `calculationVersion: 1` when editing
404
+ one. To explicitly upgrade a draft, supply `calculationVersion: 2`, `taxBps: 0`
405
+ and a confirmed `taxConfig` for every line. Finalized history cannot be upgraded.
406
+
248
407
  All invoice money fields ending in `Minor` are integer strings. `paid` is a
249
408
  read-only projection of a confirmed, non-simulated GenesisPay payment; it cannot
250
409
  be set through the SDK. Finalized invoices cannot be edited. Use
251
410
  `invoices.void()` to cancel collection or `invoices.markUncollectible()` to write
252
411
  off an unpaid invoice.
253
412
 
413
+ ### Browse invoice summaries
414
+
415
+ `listSummaries` is prepared in this repository; this change does not publish an
416
+ SDK release. If your installed release lacks the method, use the authenticated
417
+ HTTP endpoint after deploying this backend change:
418
+
419
+ ```ts
420
+ const response = await fetch(`${baseUrl}/api/v1/invoices/summaries?limit=25`, {
421
+ headers: { Authorization: `Bearer ${sellerApiKey}` },
422
+ });
423
+ if (!response.ok) throw new Error(`Invoice list failed: ${response.status}`);
424
+ const summaries = await response.json();
425
+ ```
426
+
427
+ Use `invoices.listSummaries()` for bounded pages. The default limit is 25, with
428
+ an integer maximum of 100. Pass the opaque `nextCursor` back as `after`; a null
429
+ cursor means the end. Ordering is newest first by creation time and ID, so
430
+ inserts above your current page do not duplicate older rows. This is a live
431
+ list: status can change between requests.
432
+
433
+ ```ts
434
+ let page = await genesispay.invoices.listSummaries({ limit: 25 });
435
+ for (const invoice of page.invoices) {
436
+ console.log(invoice.publicId, invoice.customer.name, invoice.totalMinor);
437
+ }
438
+ if (page.nextCursor) {
439
+ page = await genesispay.invoices.listSummaries({ limit: 25, after: page.nextCursor });
440
+ }
441
+ if (page.invoices[0]) {
442
+ const fullInvoice = await genesispay.invoices.retrieve(page.invoices[0].publicId);
443
+ }
444
+ ```
445
+
446
+ Each summary has `id`, `publicId`, nullable `invoiceNumber`, `status`, `asset`,
447
+ `chainId`, exact integer-string `totalMinor`, `customer: { name, companyName }`,
448
+ `dueAt`, `createdAt`, and nullable `paidAt`. Finalized customer names come from
449
+ the issued snapshot. A summary intentionally has no line items, delivery
450
+ history, seller profile or payment detail; retrieve an invoice to read those.
451
+ The existing `invoices.list()` still returns its newest 100 full invoices with
452
+ every nested line and delivery. It does not accept pagination options.
453
+
454
+ ### Hosted checkout documents
455
+
456
+ Every new confirmed, non-simulated hosted human checkout creates one immutable
457
+ invoice identity and two PDF artifacts: invoice and payment receipt. New human
458
+ authorizations require complete, attested KYB-approved issuer details and an
459
+ explicit item tax assertion; missing configuration blocks checkout, not silently
460
+ zero tax. Historical v1 enhanced receipts remain readable.
461
+ GenesisPay does not determine the seller's tax rate or require an Avalara or
462
+ Stripe Tax account. This archive is separate
463
+ from seller-authored `invoices`: do not try to create, edit, or number these
464
+ documents through the SDK.
465
+
466
+ ```ts
467
+ const documents = await genesispay.checkoutDocuments.list();
468
+ const oneDocument = await genesispay.checkoutDocuments.get("doc_…");
469
+
470
+ for (const document of documents) {
471
+ console.log(document.kind, document.invoiceNumber, document.totalMinor);
472
+ // document.snapshot is the buyer/seller/tax snapshot frozen before payment.
473
+ }
474
+ ```
475
+
476
+ Amounts ending in `Minor` are integer strings. A `null` `invoiceNumber` means
477
+ the document is an enhanced receipt, not an invoice with a missing number.
478
+ `checkoutDocuments.list()` returns the seller's newest 100 documents.
479
+ The authenticated PDF download is
480
+ `GET /api/v1/checkout-documents/:publicId/pdf` (invoice) and
481
+ `GET /api/v1/checkout-documents/:publicId/pdf?kind=receipt` (payment receipt).
482
+ V2 snapshots include per-line inclusive totals, item treatment, and the reviewed
483
+ seller profile version. Neither document creates another payable invoice.
484
+
254
485
  ### Subscriptions
255
486
 
256
487
  Plans are the reusable template you create once and hand to any number of
@@ -273,10 +504,38 @@ further signatures and emit `subscription.renewed` (or `subscription.past_due`).
273
504
  `genesispay.mandates.*` exposes the per-payer authorizations underneath —
274
505
  `create`, `retrieve`, `charge` (per-use metering) and `revoke`.
275
506
 
507
+ Every per-use charge requires a stable idempotency key. Reuse it after any
508
+ timeout or 503; never generate a second key for the same metered operation:
509
+
510
+ ```ts
511
+ await genesispay.mandates.charge(mandateId, {
512
+ amount: "0.08",
513
+ idempotencyKey: usageRequestId,
514
+ resourceUrl: "https://api.example/forecast",
515
+ });
516
+ ```
517
+
518
+ If the pull was submitted but its receipt is not definitive, the promise rejects
519
+ with `GenesisPayMandateChargePendingError`. Its `status`, `code` and `charge`
520
+ fields preserve the API's 503 locator (including `charge.txHash` when known), so
521
+ the caller can record the unknown outcome and retry only with the same key.
522
+
523
+ `createMandateGate` likewise requires the protected request to carry
524
+ `Idempotency-Key`. An outcome-unknown pull stays 503 with its charge locator and
525
+ the wrapped handler is not opened until the original charge is settled. The
526
+ gate treats HTTP success as transport only: the response must also name a
527
+ settled charge, the exact requested integer minor amount and a full transaction
528
+ hash. A missing, submitted, hashless or contradictory 2xx body fails closed as
529
+ 503 and the same idempotency key remains authoritative.
530
+
276
531
  There is deliberately **no `mandates.activate`**: activation requires the payer's
277
532
  own signature over the permit, so it belongs in the payer's frontend, not in a
278
533
  server holding your secret key.
279
534
 
535
+ `MandateStatus` also includes terminal `cancelled`: GenesisPay stopped an
536
+ unbroadcast permit because seller eligibility changed. That authority never
537
+ wakes after recovery; create a new proposal and obtain a fresh payer signature.
538
+
280
539
  **Cancelling a subscription** — list the plan's subscribers, find the payer, revoke:
281
540
 
282
541
  ```ts
@@ -314,7 +573,8 @@ and you get the same link with `created: false`.
314
573
  ```ts
315
574
  const product = await genesispay.products.create({
316
575
  name: "Market data report",
317
- price: "2.00",
576
+ price: "2.00", // inclusive customer total
577
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 2000, note: null },
318
578
  sku: "MDR-1",
319
579
  // A redirect product creates a signed, expiring entitlement on every sale.
320
580
  delivery: {
@@ -330,6 +590,61 @@ const { link, created } = await genesispay.products.createPaymentLink(
330
590
  // Share link.payUrl — every sale of this product settles through it.
331
591
  ```
332
592
 
593
+ #### Item tax and existing catalogue entries
594
+
595
+ `taxConfig` is `{ version: 1, treatment, rateBps, note }`. `rateBps` is an integer
596
+ (2000 = 20%), from 1 to 10000 for `taxable`. `zero_rated`, `exempt`, and
597
+ `not_collected` require rate 0 and a nonempty seller-provided explanation in
598
+ `note`. There is no automatic reverse charge or country-based tax determination.
599
+ Omitted tax on legacy API product/link creation remains **unconfigured**, not 0%;
600
+ new human checkout cannot start until the item and verified seller are ready.
601
+ Agent/x402 behavior remains separate. `checkout.create` accepts the same taxConfig.
602
+
603
+ Publish a tax correction using the exact configuration you last read:
604
+
605
+ ```ts
606
+ await genesispay.products.update(product.publicId, {
607
+ expectedTaxConfig: product.taxConfig, // null for an unconfigured product
608
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 1000, note: null },
609
+ });
610
+ // Standalone links only; product-backed links are changed through products.update:
611
+ await genesispay.checkout.updateTax(link.publicId, {
612
+ expectedTaxConfig: link.taxConfig,
613
+ taxConfig: { version: 1, treatment: "taxable", rateBps: 1000, note: null },
614
+ });
615
+ ```
616
+
617
+ The server atomically publishes product tax to its live link without repricing
618
+ it. A concurrent edit returns 409: re-read and review before retrying. Existing
619
+ payment snapshots and issued documents never change. Archived and manual-invoice
620
+ links reject generic tax updates.
621
+
622
+ For an immutable per-order checkout, assert the complete current product
623
+ contract and create an explicitly identified single-use link. Both calls are
624
+ versioned; creation requires seller-account-scoped idempotency, so API-key
625
+ rotation does not break a retry:
626
+
627
+ ```ts
628
+ await genesispay.products.assertContract(expectedProductContract);
629
+
630
+ const checkout = await genesispay.products.createCheckout(
631
+ {
632
+ expected: expectedProductContract,
633
+ clientReferenceId: order.id,
634
+ metadata: { buyerId: order.buyerId },
635
+ returnUrl: "https://shop.example.com/thanks",
636
+ },
637
+ { idempotencyKey: `product-checkout-${order.id}` },
638
+ );
639
+
640
+ checkout.linkId; // GenesisPay link identity — there is no ambiguous checkout.id
641
+ checkout.payUrl;
642
+ ```
643
+
644
+ The checkout copies the product's tax configuration inside the same transaction
645
+ that freezes its price and delivery contract. Tax never changes the signed gross
646
+ amount or serves as fulfilment authority.
647
+
333
648
  #### Never hardcode `link.payUrl`
334
649
 
335
650
  The URL embeds the link's `inv_…` id, and that id is **per link, not per
@@ -357,22 +672,20 @@ A confirmed purchase fires the `product.purchased` webhook. Its payload
357
672
  carries the buyer's entitlement — `entitlement.redemptionPath` is a signed,
358
673
  expiring redirect (~30 days) to your fulfilment URL, with `gp_*` parameters
359
674
  (`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.
675
+ via the tolerant entitlement view for migration and display. Those parameters,
676
+ the webhook object, and `entitlements.verify` are not fulfilment authority in
677
+ 1.0. Test-mode purchases still deliver end to end with an explicit simulation
678
+ marker, but only the strict verifier can return an authority-bearing success.
363
679
 
364
- Use the SDK rather than trusting `gp_*` query parameters from the browser:
680
+ Use the strict authority verifier rather than trusting `gp_*` query parameters
681
+ or the tolerant entitlement view:
365
682
 
366
683
  ```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.
684
+ const verified = await genesispay.fulfillment.verify({
685
+ locator: { entitlementId },
686
+ expected: expectedProductContract,
687
+ });
688
+ if (verified.verified) await fulfilOnce(verified.payment.attemptId);
376
689
  ```
377
690
 
378
691
  ### Product-backed API gate
@@ -400,13 +713,26 @@ Protect the route with that product. Validate request shape before calling
400
713
  `protect`, and make handler effects idempotent by `purchase.payment.attemptId`:
401
714
 
402
715
  ```ts
716
+ import { createGateRequestFingerprint } from "@genesis-tech/genesispay-seller";
717
+
403
718
  const forecastGate = genesispay.products.gate("prod_...");
404
719
 
405
720
  export async function POST(request: Request) {
406
721
  const rawBody = await request.clone().text();
407
722
  validateForecastJson(rawBody); // invalid requests never create a payment attempt
408
723
 
409
- return forecastGate.protect(request, async (_request, purchase) => {
724
+ const expectedForRequest = {
725
+ ...expectedForecastContract,
726
+ delivery: {
727
+ ...expectedForecastContract.delivery,
728
+ gate: {
729
+ ...expectedForecastContract.delivery.gate,
730
+ fingerprint: await createGateRequestFingerprint(request),
731
+ },
732
+ },
733
+ };
734
+
735
+ return forecastGate.protect(request, expectedForRequest, async (_request, purchase) => {
410
736
  const cached = await readForecast(purchase.payment.attemptId);
411
737
  if (cached) return Response.json(cached);
412
738
 
@@ -417,19 +743,95 @@ export async function POST(request: Request) {
417
743
  }
418
744
  ```
419
745
 
420
- On the initial request, `protect` returns the standard `402 Payment Required`.
746
+ `protect` validates the method, canonical resource URL, and request fingerprint
747
+ against the expected immutable contract before negotiation. Its challenge sends
748
+ that complete contract to GenesisPay, which compares it with the exact frozen
749
+ payable link before creating an attempt or returning a `402 Payment Required`;
750
+ the SDK never reconstructs authority from mutable catalogue presentation.
421
751
  GenesisPay creates a pending attempt before that response and advertises a
422
752
  reserved `gp_attempt` value in the x402 resource URL. On the signed retry, the
423
753
  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.
754
+ inputs. After settlement it retrieves strict evidence by attempt ID and invokes
755
+ the handler only after every authority field matches. If evidence is unavailable
756
+ it returns a recoverable `503` carrying `GENESISPAY-Payment-Attempt-Id` and
757
+ preserves a matching `PAYMENT-RESPONSE`. A transient response is `retryable: true` and
758
+ carries `Retry-After: 2`; a permanent inconsistency is `retryable: false` and
759
+ deliberately carries no retry instruction. The gate transport also negotiates
760
+ `GENESISPAY-Version: 2026-08-26`, while unversioned 0.x gate traffic keeps its
761
+ legacy wire contract. If the settlement connection drops after commit, the SDK
762
+ recovers the untrusted attempt locator from the signed x402 payload and returns
763
+ the same retryable 503; only `fulfillment.verify` can turn that locator into
764
+ authority. Handlers may execute more than once and must keep their own result
765
+ cache.
766
+
767
+ An interrupted verification response stream is transient; a completed malformed
768
+ evidence response is permanent. A settlement naming a different attempt drops
769
+ that unrelated receipt and reports `settlement_outcome_unknown` with the signed
770
+ attempt locator. Missing strict settlement metadata or a negative
771
+ `not_found`/`not_confirmed` verification likewise cannot claim confirmation.
772
+ Only independently verified evidence allows the handler to run.
773
+
774
+ You may retry the original signed request after its authorization expires if
775
+ that attempt was already confirmed. GenesisPay verifies the persisted attempt,
776
+ signature and original on-chain payment before replaying success; it does not
777
+ broadcast or collect a fee again. An unpaid expired authorization remains
778
+ rejected. Keep the same attempt locator and signature when recovering a payment.
779
+ Failed/expired attempts are terminal even if their signature is still valid or
780
+ the original link has been archived (`409 attempt_failed` / `attempt_expired`).
427
781
 
428
782
  `products.list({ includeArchived: true })`, `products.retrieve`,
429
783
  `products.archive` complete the namespace. Archiving stops **new** link mints;
430
784
  the existing canonical link stays payable. The price is copied onto the link
431
785
  at mint — a later catalogue edit never changes what a buyer already sees.
432
786
 
787
+ #### Prepare requests for body-priced resources
788
+
789
+ Before an agent signs, it asks your resource to freeze the exact settlement
790
+ plan: a request carrying `GENESISPAY-Settlement-Prepare: 1`. A client that
791
+ puts its plan parameters in a second header,
792
+ `GENESISPAY-Settlement-Prepare-Params` (base64 JSON
793
+ `{ payer, idempotencyKey, feeMode, authority? }`, decoded by
794
+ `decodeSettlementPrepareParamsHeader` from `@genesis-tech/genesispay-protocol`),
795
+ sends that preparation as a **repeat of the purchase request** — same method,
796
+ URL (plus the reserved `gp_attempt` query), `content-type` and body.
797
+
798
+ **Rollout:** the GenesisPay agent engine does not send the params header yet;
799
+ that is a server follow-up. Until it ships, agents send the legacy form
800
+ described below, so a body-validated route must still accept the legacy
801
+ preparation body — or hand any request carrying
802
+ `GENESISPAY-Settlement-Prepare: 1` to `protect` before its own validation.
803
+
804
+ For a client that sends the header, your route needs no special case. Parse and
805
+ validate the body as for any purchase — read it from `request.clone()` (or pass
806
+ a re-created `Request`) so the gate can still read the body and recompute the
807
+ fingerprint; a consumed body answers `422 { code: "invalid_request" }` — select
808
+ the gate for it (for example the generic gate for a
809
+ request tier, or the product whose registered resource serves that tier) and
810
+ call `protect` or the wrapped handler. A body-priced API needs one gate
811
+ resource per price: a product gate is unique per method and resource URL, so
812
+ each tier is its own registered resource (or its own generic gate). The gate
813
+ recognises the preparation by its header, never runs your handler for it, and
814
+ answers with the plan:
815
+
816
+ - **Product gates** recompute the method, canonical resource URL and request
817
+ fingerprint and require them to equal your expected gate intent, exactly as
818
+ for the challenge and the signed retry. A preparation for a different request
819
+ answers `422 { code: "contract_mismatch", mismatches }` and nothing reaches
820
+ GenesisPay. The forwarded `prepare` action is unchanged.
821
+ - **The generic gate** (`createPaymentGate`) prices by configuration and binds
822
+ no request fingerprint; it reads the parameters from the header and treats
823
+ the body as opaque purchase content.
824
+ - A present but malformed params header answers
825
+ `422 { code: "invalid_request" }`; the gate never falls back to the body.
826
+
827
+ Without the params header, both gates keep the legacy form, in which the body
828
+ carries the plan parameters. It stays supported for GET resources, hosted
829
+ payment links and the current agent engine — but a route that rejects any body
830
+ other than its own purchase schema will refuse it, so such a route must
831
+ recognise the prepare header before its own validation until clients send the
832
+ header form. The decision is
833
+ recorded in ADR-0083 of the GenesisPay repository.
834
+
433
835
  ### Testing the paid path
434
836
 
435
837
  ```ts
@@ -439,8 +841,9 @@ const session = await genesispay.checkout.simulatePayment(publicId);
439
841
 
440
842
  Test keys only — a `gp_sk_live_…` key gets a 403, and on a mainnet deployment the
441
843
  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.
844
+ transaction happened) and `simulated: true` in tolerant display responses. Never
845
+ use that response to fulfil: strict `fulfillment.verify` returns the normal
846
+ negative reason `simulated`, while a pending real attempt can also have no hash.
444
847
 
445
848
  ### Fulfilment guide
446
849
 
@@ -452,15 +855,77 @@ handler run never double-delivers.
452
855
 
453
856
  | Product | Fulfil on | Deduplicate by |
454
857
  |---|---|---|
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` |
858
+ | Standalone single-use checkout | strict `fulfillment.verify` by link or attempt | confirmed `payment.attemptId` |
859
+ | Product without digital delivery | strict `fulfillment.verify` by attempt | `payment.attemptId` — never link-level `paid` |
860
+ | Redirect product | strict `fulfillment.verify` by entitlement or attempt | `payment.attemptId` |
861
+ | Product-backed gate | strict evidence returned after confirmed settlement | `payment.attemptId` |
862
+
863
+ `payment.fulfilled` is the canonical notification. Its `data` is the strict
864
+ FulfillmentEvidence wire shape plus explicit nullable `clientReferenceId`; its
865
+ envelope has `apiVersion: "2026-08-26"` and `livemode`. Simulation does not emit
866
+ it. `constructEvent` verifies raw bytes before parsing and validates known evidence
867
+ fields, preserving integer amount strings. It does **not** return VerifiedPayment:
868
+ always call authenticated `fulfillment.verify({ locator, expected })` afterward.
869
+ The name does not claim that your application has delivered anything.
870
+
871
+ `livemode` is not present on every event type. Do not discard an event because
872
+ `!event.livemode` is true: an absent field also passes that check. For
873
+ `payment.fulfilled`, `livemode: false` means Base Sepolia, and
874
+ `data.simulation.simulated` is explicitly false. Network and simulation are
875
+ separate facts; never substitute `livemode` for the event's simulation field.
876
+ Use authenticated `fulfillment.verify` before granting credits or fulfilling
877
+ an order.
878
+
879
+ Persist a purchase intent (account, credits, expected contract) before checkout.
880
+ Use its ID as `clientReferenceId` and the checkout idempotency key. Store the
881
+ returned link ID and require `verified.payment.linkId` to match it before credit.
882
+ If the webhook precedes that response, keep it pending and recover the same
883
+ checkout; never assign a payment to an account from webhook metadata alone.
884
+
885
+ New event IDs are stable across endpoints, retries and explicit replay. Dedupe
886
+ inbox processing by event ID, but **atomically claim verified attempt ID and
887
+ purchase intent with the credit balance/ledger update** across every trigger.
888
+ Different event types, browser return and reconciliation must converge there.
889
+ Keep a conflicting binding for investigation; never grant a second credit.
890
+ Persist the inbox before acknowledging 2xx and let a worker retry verification.
891
+
892
+ There are nine sends per delivery cycle: initial plus 30s, 2m, 5m, 15m, 1h, 6h,
893
+ 12h, 24h delays (±10% jitter), maximum72h. Minute scheduling rounds due work up to
894
+ the next tick. Only explicit audited operator replay opens another cycle, with
895
+ identical body and event ID. Acknowledgement loss permits duplicates. Outbox
896
+ producers/worker must be deployed and measured before relying on these targets.
897
+
898
+ ### Recover missing notifications
899
+
900
+ ```ts
901
+ let cursor: string | undefined;
902
+ do {
903
+ const page = await genesispay.fulfillment.listAttempts({
904
+ clientReferenceId: intent.id,
905
+ confirmedAfter: intent.createdAt,
906
+ limit: 50,
907
+ cursor,
908
+ });
909
+ for (const candidate of page.data) {
910
+ const verified = await genesispay.fulfillment.verify({
911
+ locator: { attemptId: candidate.attemptId }, expected: intent.expected,
912
+ });
913
+ if (verified.verified && verified.payment.linkId === intent.linkId) {
914
+ // Your shared transaction claims attempt + intent and writes credits.
915
+ await creditVerifiedPurchaseOnce(intent, verified.payment);
916
+ }
917
+ }
918
+ cursor = page.nextCursor ?? undefined;
919
+ } while (cursor);
920
+ ```
459
921
 
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).
922
+ Listing requires the strict API version and seller key and enforces seller/mode
923
+ scope. Optional exact reference (max200), inclusive ISO confirmedAfter/Before,
924
+ limit1..100(default50) and opaque cursor are supported. Keep filters unchanged
925
+ between pages. A cursor fixes the upper bound and preserves database microseconds.
926
+ Repeat scans for late commits; include abandoned, expired and cancelled browser
927
+ flows. `authorityVersion: null` is a historical diagnostic, never permission to
928
+ credit. No listing row is authority, even when it names a confirmed attempt.
464
929
 
465
930
  ### Correlation, and the `cs` query parameter
466
931
 
@@ -492,18 +957,27 @@ The `returnUrl`/`cancelUrl` limits and the amount-conflict check are validated
492
957
  remaining limits (`metadata`, `clientReferenceId`) are enforced server-side and
493
958
  fail the `checkout.create` call with a 422.
494
959
 
495
- ### Note on `paid` for reusable links
960
+ ### Note on `paid` for product links
496
961
 
497
962
  `session.paid` is derived from `confirmedPaymentCount > 0`. For a `reusable` link
498
963
  that counter only ever grows, so `paid` stays `true` from the first payment
499
964
  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.
965
+ For per-purchase fulfilment on a product link, use a webhook as a trigger and then
966
+ strictly verify its attempt ID. Never use `confirmedPaymentCount` as authority.
502
967
 
503
968
  Options: `baseUrl` (override the mode default — https, http only for localhost;
504
969
  both modes default to the GenesisPay facilitator, so it is optional), `configTtlMs`
505
970
  (seller-config cache TTL, default 5 min), `expectedPayTo` (recommended for `live`
506
971
  keys — a local pin that fail-closes if the resolved wallet ever differs), `fetchFn`.
972
+ The pin also applies to `checkout.retrieve`, `products.assertContract`,
973
+ `products.createCheckout`, `products.gate(...).protect`, and `fulfillment.verify`.
974
+ Checkout retrieval checks the raw destination before its display mapper runs;
975
+ strict methods refuse a conflicting expected destination before a network request.
976
+ Missing or conflicting destinations fail closed. An explicit expected contract
977
+ cannot override the client pin, including on historical verification.
978
+ After a wallet rotation, reconcile old payments with a separate client pinned
979
+ to the original destination recorded in your immutable order contract. Keep the
980
+ current client pinned to the new wallet; neither pin rewrites payment history.
507
981
  The client fails **closed**: an unreachable backend returns `503` and a seller with
508
982
  no wallet returns a `402` `payment_not_configured` — the paid handler never runs
509
983
  without a valid destination. It also refuses to advertise a wallet whose network
@@ -596,7 +1070,10 @@ The same wrapped handler drops straight into `Bun.serve({ fetch: gated })`.
596
1070
 
597
1071
  `gate.wrap(handler, { verifySettlement })` requires a settlement hook. Use the
598
1072
  built-in `genesisPaySettlement({ facilitatorBaseUrl, apiKey })`, which POSTs the
599
- payment to GenesisPay's facilitator (`/api/v1/facilitator/settle`). GenesisPay
1073
+ plan-aware prepare request before an agent signs, then forwards the signed
1074
+ authorization with its immutable settlement-plan id. Clients that omit the
1075
+ prepare handshake are refused before broadcast with `settlement_plan_required`.
1076
+ The hook sends preparation and settlement to GenesisPay's facilitator. GenesisPay
600
1077
  broadcasts the EIP-3009 authorization on-chain, verifies the USDC transfer,
601
1078
  and returns the receipt. `facilitatorBaseUrl` is optional and defaults to the
602
1079
  public development facilitator (`DEFAULT_FACILITATOR_BASE_URL`,
@@ -630,3 +1107,105 @@ const verifySettlement: VerifySettlement = async ({ payment, requirement }) => {
630
1107
  your `verifySettlement` hook.
631
1108
  - Get a seller API key (`gp_sk_...`) from your GenesisPay dashboard under
632
1109
  Developers.
1110
+
1111
+ ### Beta: new mandate authority answers 409
1112
+
1113
+ During the GenesisPay beta, a deployment may lock the creation of **new** mandate
1114
+ authority. While it is locked, `mandates.create`, the `contract_mandate_v1`
1115
+ helpers (`mandates.createContractSubscription`, `mandates.createContractPerUse`,
1116
+ `mandates.proposeContractUsage`) and `plans.create` reject with HTTP `409` and
1117
+ code `mandate_authority_locked_for_beta`. There is no `Retry-After` — waiting
1118
+ does not change the answer, so treat it as a capability that is off rather than a
1119
+ transient failure.
1120
+
1121
+ `mandates.submitContractUsage`, `mandates.getContractUsage`, revocation, every
1122
+ list/read, and the whole x402 payment-gate path are unaffected, as is any mandate
1123
+ a payer already approved. A retry of a request made before the lock still returns
1124
+ its original charge, mandate or payment identity — keep using the SAME
1125
+ idempotency key, exactly as you would without the lock. In particular a
1126
+ `mandates.charge` retry whose key already names a charge still comes back with
1127
+ that charge (settled, or `submitted` with its locator) rather than the 409, so a
1128
+ `409 mandate_authority_locked_for_beta` always means no charge exists for that
1129
+ key. Never retry with a fresh key to work around it.
1130
+
1131
+ ### Experimental gated contract subscriptions
1132
+
1133
+ `mandates.createContractSubscription` explicitly requests the two-signature
1134
+ `contract_mandate_v1` protocol. It requires an ISO expiry and validates both
1135
+ returned signing payloads. This capability is off by default; the reviewed
1136
+ contract registry must permit the deployment. Mainnet also requires legal/audit
1137
+ and completed legacy-authority cutover evidence.
1138
+
1139
+ ```ts
1140
+ const proposal = await genesispay.mandates.createContractSubscription({
1141
+ payerWallet, allowance: "108", capPerCharge: "9", amountPerPeriod: "9",
1142
+ periodDays: 30, validUntil: "2027-09-01T00:00:00Z",
1143
+ });
1144
+ // In the payer frontend, sign approval.termsTypedData, then approval.permitTypedData.
1145
+ // POST /api/v1/mandates/:id/activate:
1146
+ // { protocol: "contract_mandate_v1", termsSignature, permitSignature }
1147
+ ```
1148
+
1149
+ HTTP202 is acceptance, not activation or payment confirmation. Retry the same
1150
+ signatures after an unknown HTTP outcome. A409 `mandate_proposal_stale` requires a
1151
+ new proposal and two new approvals. The legacy `mandates.create` method keeps its
1152
+ one-permit contract and cannot silently switch protocols.
1153
+
1154
+ `mandates.revoke(id)` requests local cancellation for contract mandates; inspect
1155
+ `revocationStatus`. The payer signs `revocationTypedData` from the returned API
1156
+ mandate and any relay can POST `{ protocol: "contract_mandate_v1", signature }`
1157
+ to `/api/v1/mandates/:id/revoke-signed`. Only `confirmed` means the permanent
1158
+ on-chain revocation exists. An independent relay may instead call the bound
1159
+ executor's `revokeWithSignature(payer, mandateId, signature)`; the payer can call
1160
+ `revoke(mandateId)` directly. Neither path requires an API session. Do not use
1161
+ `approve(0)` as proof that old signed permits are invalid.
1162
+
1163
+ Contract subscription renewals are admitted by the worker or cron and become paid
1164
+ only after canonical atomic receipt verification. First charge is eligible at
1165
+ activation; later charges wait the signed interval after successful settlement.
1166
+ Downtime does not create catch-up charges. A confirmed revert may receive a new
1167
+ attempt only after exact receipt and unused-authority verification.
1168
+
1169
+ Contract billing webhooks add `protocol: "contract_mandate_v1"`, `contractEvent`
1170
+ (immutable chain/block/log, logical identity, sequence and split amounts), and
1171
+ `mandateStateContext: { kind: "projection_snapshot", observedAt }`. `occurredAt`
1172
+ is the event's block time; `mandate` is the state when that event was projected,
1173
+ which can be later. Deliveries may arrive out of order: an old `mandate.active`
1174
+ event can carry an already revoked snapshot. Order event facts by block/log and
1175
+ retrieve current state before enabling access. Deduplicate the envelope `id`;
1176
+ replays preserve its original bytes, including the original snapshot.
1177
+
1178
+ ### Experimental gated signed per-use mandates
1179
+
1180
+ `mandates.createContractPerUse({ payerWallet, allowance, capPerCharge, validUntil })`
1181
+ returns the same two bounded approvals as the contract subscription API. It has
1182
+ no recurring amount or interval. After the payer signs both approvals and the
1183
+ activation confirms, each usage request requires its own payer signature:
1184
+
1185
+ ```ts
1186
+ const charge = await genesispay.mandates.proposeContractUsage({
1187
+ mandate: approvedMandate, // the saved createContractPerUse result
1188
+ amountMinor: "80000", // gross integer minor units, including any fee
1189
+ idempotencyKey: "forecast-request-001",
1190
+ resourceUrl: "https://example.com/forecast",
1191
+ });
1192
+ // The payer's wallet signs charge.chargeTypedData. The seller SDK never signs.
1193
+ const accepted = await genesispay.mandates.submitContractUsage(charge, payerSignature);
1194
+ const current = await genesispay.mandates.getContractUsage(accepted);
1195
+ // Fulfill only after current.status === "settled", verified against your order.
1196
+ ```
1197
+
1198
+ The SDK independently builds the charge EIP-712 message from the saved mandate
1199
+ consent and exact requested gross. It checks chain, executor, mandate, fee,
1200
+ request metadata and signing fields. Submission and polling preserve the original
1201
+ charge identity and deadline. HTTP202 means pending; queue acceptance is not a
1202
+ confirmed payment. A retry uses the same idempotency key and saved proposal.
1203
+ Changing a request under that key returns a conflict, including after failure.
1204
+
1205
+ The server retains the proposal before a wallet prompt. If no signature arrives,
1206
+ it closes the source only after finalized proof that the charge is unused and
1207
+ its authority expired or was revoked. The old `mandates.charge` and metering gate
1208
+ are legacy-only and refuse contract mandates; they cannot silently replace the
1209
+ required signature. These contract APIs remain gated and unreleased on Mainnet.
1210
+ Agent policy/signing integration and per-use successors after a mined revert
1211
+ remain separate pending implementation work.