@genesis-tech/genesispay-seller 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -38
- package/README.md +142 -40
- package/dist/client.d.ts +23 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +73 -39
- package/dist/client.js.map +1 -1
- package/dist/customers.d.ts +2 -2
- package/dist/customers.d.ts.map +1 -1
- package/dist/customers.js +5 -5
- package/dist/customers.js.map +1 -1
- package/dist/entitlements.d.ts +19 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +30 -0
- package/dist/entitlements.js.map +1 -0
- package/dist/errors.d.ts +9 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -18
- package/dist/errors.js.map +1 -1
- package/dist/genesispay-settlement.d.ts +30 -0
- package/dist/genesispay-settlement.d.ts.map +1 -0
- package/dist/genesispay-settlement.js +103 -0
- package/dist/genesispay-settlement.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/invoices.d.ts +2 -2
- package/dist/invoices.d.ts.map +1 -1
- package/dist/invoices.js +7 -7
- package/dist/invoices.js.map +1 -1
- package/dist/mandate-gate.d.ts +5 -5
- package/dist/mandate-gate.d.ts.map +1 -1
- package/dist/mandate-gate.js +4 -4
- package/dist/mandate-gate.js.map +1 -1
- package/dist/mandates.d.ts +4 -4
- package/dist/mandates.d.ts.map +1 -1
- package/dist/mandates.js +9 -9
- package/dist/mandates.js.map +1 -1
- package/dist/payment-gate.js +1 -1
- package/dist/payment-gate.js.map +1 -1
- package/dist/plans.d.ts +2 -2
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +6 -6
- package/dist/plans.js.map +1 -1
- package/dist/products.d.ts +128 -0
- package/dist/products.d.ts.map +1 -0
- package/dist/products.js +287 -0
- package/dist/products.js.map +1 -0
- package/dist/resource.d.ts +2 -2
- package/dist/resource.d.ts.map +1 -1
- package/dist/resource.js +1 -1
- package/dist/resource.js.map +1 -1
- package/dist/webhooks.d.ts +25 -25
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +30 -29
- package/dist/webhooks.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0 — product-backed x402 resource gates
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`delivery` on `genesispay.products.create()` and `.update()`**. A product
|
|
8
|
+
can now be a redirect entitlement, a registered HTTP resource gate, or have
|
|
9
|
+
no delivery. The flat `fulfilmentUrl` fields remain compatible with existing
|
|
10
|
+
redirect integrations.
|
|
11
|
+
- **`genesispay.products.gate(productId)`**. `prime()` resolves the registered
|
|
12
|
+
gate product and canonical reusable payment link; `protect(request, handler)`
|
|
13
|
+
performs the x402 challenge/settlement exchange and runs the handler only
|
|
14
|
+
after GenesisPay confirms payment. Use `purchase.payment.attemptId` as the
|
|
15
|
+
idempotency key because confirmed payment retries may run a handler again.
|
|
16
|
+
- **`genesispay.entitlements.verify(entitlementId)`**. This seller-authorized
|
|
17
|
+
hard check is for a redirect product's redemption handler; require a valid,
|
|
18
|
+
non-simulated entitlement for the expected product before granting access.
|
|
19
|
+
|
|
20
|
+
### Safety
|
|
21
|
+
|
|
22
|
+
- Product gates bind the pending attempt to the registered URL, upper-case HTTP
|
|
23
|
+
method, and a SHA-256 fingerprint of the raw request body. GenesisPay receives
|
|
24
|
+
the fingerprint, not the request data.
|
|
25
|
+
- Gate purchases never mint redirect entitlements. Redirect products retain
|
|
26
|
+
their signed 30-day redemption URLs unchanged.
|
|
27
|
+
- A product gate always derives amount, asset, destination and fees from the
|
|
28
|
+
canonical product link; callers cannot supply those money fields.
|
|
29
|
+
|
|
30
|
+
## 0.9.0 — BREAKING: every identifier is now `genesispay`
|
|
31
|
+
|
|
32
|
+
The legacy/visible name split is retired. This release renames the
|
|
33
|
+
public surface with **no compatibility window** — the old names are not accepted
|
|
34
|
+
alongside the new ones. Update all of the following at once:
|
|
35
|
+
|
|
36
|
+
- Client and errors renamed: `PeerPay` → `GenesisPay`, `PeerPay*Error` →
|
|
37
|
+
`GenesisPay*Error` (including `PeerPaySignatureVerificationError`).
|
|
38
|
+
- Webhook signature header `PEERPAY-SIGNATURE` → `GENESISPAY-SIGNATURE`.
|
|
39
|
+
**Verification fails silently if you do not update this** — the old header
|
|
40
|
+
simply stops arriving.
|
|
41
|
+
- Seller API keys now use the `gp_sk_` prefix. Keys issued as `pp_sk_` are
|
|
42
|
+
rejected and must be reissued from the dashboard.
|
|
43
|
+
- Environment variables `PEERPAY_*` → `GENESISPAY_*`.
|
|
44
|
+
- Checkout return parameters `peerpay_link_id` / `peerpay_status` →
|
|
45
|
+
`genesispay_link_id` / `genesispay_status`.
|
|
46
|
+
- `genesispay-settlement` module renamed to `genesispay-settlement`.
|
|
47
|
+
|
|
48
|
+
### Also in this release — a real fix, not a rename
|
|
49
|
+
|
|
50
|
+
`PRODUCTION_FACILITATOR_BASE_URL` pointed at `peerpay-app-production.up.railway.app`,
|
|
51
|
+
a Railway-generated hostname that **never existed** — production has exactly one
|
|
52
|
+
domain. Any live key used via the zero-config path (`new GenesisPay({ apiKey })`
|
|
53
|
+
with no `baseUrl`) therefore resolved to a dead host. Both defaults now use the
|
|
54
|
+
custom domains, which are ours and survive a Railway service rename:
|
|
55
|
+
|
|
56
|
+
- live keys → `https://genesispay.finance`
|
|
57
|
+
- `DEFAULT_FACILITATOR_BASE_URL` → `https://dev.genesispay.finance`
|
|
58
|
+
|
|
59
|
+
Everything else in this release is a rename.
|
|
60
|
+
|
|
3
61
|
## Unreleased — renamed to `@genesis-tech/genesispay-seller`
|
|
4
62
|
|
|
5
|
-
The `@genesis-tech/
|
|
6
|
-
never published to npm, so the first release
|
|
7
|
-
(ADR-0044). Every version below describes work done under the old name.
|
|
63
|
+
The legacy `@genesis-tech/genesispay-seller` name was versioned here through 0.8.0
|
|
64
|
+
but never published to npm, so the first npm release carried the product's own
|
|
65
|
+
name (ADR-0044). 0.8.0 was published on 2026-08-07. Every version below describes work done under the old name.
|
|
8
66
|
|
|
9
67
|
## 0.8.0
|
|
10
68
|
|
|
@@ -13,9 +71,9 @@ subscription surfaces.
|
|
|
13
71
|
|
|
14
72
|
### Added
|
|
15
73
|
|
|
16
|
-
- **`
|
|
74
|
+
- **`genesispay.customers.*`** — create, list, retrieve, update, and archive
|
|
17
75
|
reusable billing contacts.
|
|
18
|
-
- **`
|
|
76
|
+
- **`genesispay.invoices.*`** — create/update drafts, finalize an immutable invoice,
|
|
19
77
|
retrieve/list it, send it through the configured email provider, void it, or
|
|
20
78
|
mark it uncollectible.
|
|
21
79
|
- Finalized invoice responses include the hosted invoice URL, PDF URL, exact
|
|
@@ -40,7 +98,7 @@ Additive and backward compatible; no existing handler changes meaning.
|
|
|
40
98
|
### Added
|
|
41
99
|
|
|
42
100
|
- **`amount` and `asset` on the `payment.confirmed` / `link.paid` payload**
|
|
43
|
-
(`
|
|
101
|
+
(`GenesisPayPaymentEventData["link"]`). `event.data.link.asset` names the
|
|
44
102
|
currency; `event.data.link.amount` is the field whose name matches its value.
|
|
45
103
|
`amountUsdcMinor` is unchanged — same integer minor units, same name.
|
|
46
104
|
- **`attempt.chainId` on the same payload** — the chain `attempt.txHash` is on.
|
|
@@ -101,14 +159,14 @@ and backward compatible: nothing is removed and no existing call changes meaning
|
|
|
101
159
|
### Behaviour changes
|
|
102
160
|
|
|
103
161
|
- `checkout.create` with **both** `amount` and `amountUsdc` set to *different*
|
|
104
|
-
amounts throws `
|
|
162
|
+
amounts throws `GenesisPayValidationError` and never sends the request. Equal
|
|
105
163
|
values are fine — `"5.0"` and `"5.00"` compare as the same money, since the
|
|
106
164
|
check is on minor units rather than on the strings. The server enforces the
|
|
107
165
|
same rule (422) for callers that do not use this SDK.
|
|
108
166
|
- `checkout.create` with **neither** field is still a **compile** error:
|
|
109
167
|
`CheckoutAmountInput` is a union requiring one of the two names, so the
|
|
110
168
|
guarantee the required `amountUsdc` property gave is not traded away. It also
|
|
111
|
-
throws `
|
|
169
|
+
throws `GenesisPayValidationError` at runtime, for JavaScript callers.
|
|
112
170
|
- Every create request sends the amount under **both** names. A backend older
|
|
113
171
|
than 0.6.0 only knows `amountUsdc` and ignores `amount`, so 0.6.0 works
|
|
114
172
|
unchanged against one.
|
|
@@ -132,7 +190,7 @@ real operation.
|
|
|
132
190
|
|
|
133
191
|
### Added
|
|
134
192
|
|
|
135
|
-
- **`
|
|
193
|
+
- **`genesispay.mandates.list({ planId?, status?, limit?, startingAfter? })`** —
|
|
136
194
|
until now a mandate id arrived only on the `mandate.active` webhook, so a
|
|
137
195
|
missed delivery meant a customer who wanted to cancel could not be served:
|
|
138
196
|
`revoke` needs that id and there was no way to look it up. Returns
|
|
@@ -143,7 +201,7 @@ real operation.
|
|
|
143
201
|
- `planId` takes the plan's **`publicId`**. An unknown or foreign plan returns
|
|
144
202
|
an empty page rather than a 404, so it cannot be used to probe for plans.
|
|
145
203
|
- **`subscriptionPlanId` on `Mandate`** and on the mandate webhook payload
|
|
146
|
-
(`
|
|
204
|
+
(`GenesisPayMandateEventData`). Without it a `mandate.active` handler knew *that*
|
|
147
205
|
someone subscribed but not *to what*. `null` means the mandate was proposed
|
|
148
206
|
directly rather than through a plan checkout — a valid state, not missing data.
|
|
149
207
|
The server sets it only in the hosted `/subscribe/:planId` flow, so a mandate
|
|
@@ -152,7 +210,7 @@ real operation.
|
|
|
152
210
|
### Behaviour changes
|
|
153
211
|
|
|
154
212
|
- A **400** whose body carries structured `issues` now throws
|
|
155
|
-
`
|
|
213
|
+
`GenesisPayValidationError` instead of `GenesisPayConfigError` — the new list
|
|
156
214
|
endpoint reports invalid query parameters that way. A 400 *without* `issues`
|
|
157
215
|
is unchanged.
|
|
158
216
|
|
|
@@ -168,18 +226,18 @@ Additive except where noted under *Behaviour changes*.
|
|
|
168
226
|
|
|
169
227
|
### Added
|
|
170
228
|
|
|
171
|
-
- **`
|
|
229
|
+
- **`genesispay.plans.*`** — `create`, `list`, `retrieve`, `archive` against the new
|
|
172
230
|
`/api/v1/plans`. Subscription plans existed before but were reachable only from
|
|
173
231
|
the dashboard, which is what actually blocked building subscriptions
|
|
174
232
|
programmatically. Every plan carries **`checkoutUrl`**, the hosted
|
|
175
233
|
`/subscribe/:publicId` flow to hand a customer — you no longer assemble it.
|
|
176
|
-
- **`
|
|
234
|
+
- **`genesispay.mandates.*`** — `create`, `retrieve`, `charge`, `revoke`. There is
|
|
177
235
|
deliberately **no `activate`**: activating a mandate needs the payer's
|
|
178
236
|
signature, not the seller key, so it belongs in the payer's frontend.
|
|
179
237
|
`mandates.create` returns `{ mandate, permitTypedData, spender }`, and
|
|
180
238
|
`permitTypedData` is passed through verbatim — a signature covers those exact
|
|
181
239
|
bytes, so the SDK must not reshape them.
|
|
182
|
-
- **`
|
|
240
|
+
- **`genesispay.checkout.simulatePayment(publicId, { payerWallet? })`** — mints a
|
|
183
241
|
confirmed payment in test mode and fires the real `payment.confirmed` /
|
|
184
242
|
`link.paid` webhooks, so the paid path is testable without testnet USDC and
|
|
185
243
|
wallet UX. Test keys only (a live key gets 403), and the endpoint does not
|
|
@@ -192,29 +250,29 @@ Additive except where noted under *Behaviour changes*.
|
|
|
192
250
|
this flag is the only honest signal. Real payments report `false`.
|
|
193
251
|
- **Typed webhook events.** `constructEvent` now returns a union discriminated on
|
|
194
252
|
`type`, so `if (event.type === "payment.confirmed")` narrows `event.data` with
|
|
195
|
-
no cast. Also exported: `
|
|
196
|
-
`
|
|
253
|
+
no cast. Also exported: `GENESISPAY_EVENT_TYPES`, `isKnownGenesisPayEventType`,
|
|
254
|
+
`GenesisPayEventType`, and `GenesisPayAnyEvent`/`GenesisPayUnknownEvent` for handlers
|
|
197
255
|
that want to model the open world.
|
|
198
256
|
- An **unknown event type is still accepted**, verified and returned — a newer
|
|
199
257
|
server must never make an older SDK reject deliveries. Only the signature or
|
|
200
258
|
a malformed envelope can reject.
|
|
201
|
-
- `
|
|
259
|
+
- `GenesisPayEvent` has no `{ type: string }` fallback member on purpose:
|
|
202
260
|
TypeScript disables discriminant narrowing for *every* member of a union as
|
|
203
|
-
soon as one member's discriminant is a non-literal. Use `
|
|
261
|
+
soon as one member's discriminant is a non-literal. Use `GenesisPayAnyEvent`
|
|
204
262
|
(assignable from any known event, no cast) when you need the open type.
|
|
205
|
-
- **`
|
|
206
|
-
and **`
|
|
263
|
+
- **`GenesisPayValidationError`** (422, with structured `issues: [{path, message}]`)
|
|
264
|
+
and **`GenesisPayRateLimitError`** (429, with `retryAfterSeconds` from
|
|
207
265
|
`Retry-After`).
|
|
208
266
|
|
|
209
267
|
### Behaviour changes
|
|
210
268
|
|
|
211
|
-
- A 422 or 429 previously surfaced as `
|
|
212
|
-
two classes above. If you branch on `
|
|
213
|
-
update that branch — both new classes extend `Error`, not `
|
|
269
|
+
- A 422 or 429 previously surfaced as `GenesisPayConfigError`. They now throw the
|
|
270
|
+
two classes above. If you branch on `GenesisPayConfigError` to catch bad input,
|
|
271
|
+
update that branch — both new classes extend `Error`, not `GenesisPayConfigError`.
|
|
214
272
|
- **`expectedPayTo` now also covers created objects.** It previously guarded only
|
|
215
273
|
the `/api/v1/seller` config lookup, while `checkout.create` and `plans.create`
|
|
216
274
|
each freeze their own `destinationWallet` — the addresses money actually moves
|
|
217
|
-
to. Both now throw `
|
|
275
|
+
to. Both now throw `GenesisPayNetworkSafetyError` on a mismatch. If you set the
|
|
218
276
|
pin and create links for a wallet other than the pinned one, those calls will
|
|
219
277
|
start failing (which is the point).
|
|
220
278
|
- **An unknown `plan.status` now degrades to `archived`, not `active`.** A status
|
|
@@ -247,21 +305,21 @@ call signature changes.
|
|
|
247
305
|
`publicId` back to a buyer.
|
|
248
306
|
- **Return / cancel URLs** — `checkout.create` accepts `returnUrl` and `cancelUrl`.
|
|
249
307
|
After a confirmed payment the hosted checkout shows an explicit "back to …"
|
|
250
|
-
button with `?
|
|
308
|
+
button with `?genesispay_link_id=<publicId>&genesispay_status=paid` appended (existing
|
|
251
309
|
query parameters on your URL are preserved). Deliberately not an automatic
|
|
252
310
|
timed redirect: the payer should be able to see the on-chain confirmation before
|
|
253
311
|
leaving. Both must be `https` (`http` is accepted for localhost only).
|
|
254
312
|
- **`checkout.retrieve(publicId)`** — reads a link's current state, including a
|
|
255
313
|
`paid` boolean and `confirmedPaymentCount`, for polling without a raw fetch
|
|
256
|
-
against `/api/v1/links/:publicId`. A 404 throws the new `
|
|
257
|
-
rather than `
|
|
314
|
+
against `/api/v1/links/:publicId`. A 404 throws the new `GenesisPayNotFoundError`
|
|
315
|
+
rather than `GenesisPayConfigError`, so an unknown id is distinguishable from a
|
|
258
316
|
broken key or an unreachable backend.
|
|
259
317
|
- **`constructEvent(rawBody, signatureHeader, secret, opts?)`** — webhook signature
|
|
260
|
-
verification, exported as a free function and as `
|
|
318
|
+
verification, exported as a free function and as `genesispay.webhooks.constructEvent`.
|
|
261
319
|
Constant-time comparison, a replay window on the signature timestamp (default
|
|
262
320
|
300 s, rejecting future timestamps as well as stale ones), and support for
|
|
263
321
|
multiple `v1=` values so an endpoint secret can be rotated without dropped
|
|
264
|
-
deliveries. Throws `
|
|
322
|
+
deliveries. Throws `GenesisPaySignatureVerificationError`; no error message
|
|
265
323
|
contains the secret or the expected signature.
|
|
266
324
|
|
|
267
325
|
### Note on `constructEvent` being async
|
|
@@ -273,7 +331,7 @@ Workers and Bun. Remember the `await`.
|
|
|
273
331
|
### Gotchas worth knowing before you upgrade
|
|
274
332
|
|
|
275
333
|
- `constructEvent` returns a **Promise** (see above).
|
|
276
|
-
- The `?
|
|
334
|
+
- The `?genesispay_link_id=…&genesispay_status=paid` parameters appended to your
|
|
277
335
|
`returnUrl` are a UI hint, **not** proof of payment — they are unsigned and a
|
|
278
336
|
payer can navigate to that URL without paying. Fulfil on the webhook or on
|
|
279
337
|
`checkout.retrieve().paid`.
|
|
@@ -298,20 +356,20 @@ Workers and Bun. Remember the `await`.
|
|
|
298
356
|
|
|
299
357
|
### Added
|
|
300
358
|
|
|
301
|
-
- **`
|
|
302
|
-
seller API key. The key prefix (`
|
|
359
|
+
- **`GenesisPay` client** — a Stripe-like entry point that configures from just the
|
|
360
|
+
seller API key. The key prefix (`gp_sk_test_…` / `gp_sk_live_…`) determines the
|
|
303
361
|
mode and base URL; the payout wallet and network are resolved from the key via the
|
|
304
|
-
new
|
|
362
|
+
new GenesisPay backend endpoint `GET /api/v1/seller` and cached (single-flight, 5-min
|
|
305
363
|
TTL, failures never cached).
|
|
306
|
-
- `
|
|
364
|
+
- `genesispay.checkout.create({ title, amountUsdc, … })` → hosted `payUrl` (the wallet
|
|
307
365
|
is defaulted server-side from the key).
|
|
308
|
-
- `
|
|
366
|
+
- `genesispay.gate({ amountUsdc }).wrap(handler)` → x402 gate with `payTo` + network
|
|
309
367
|
resolved from the key and settlement auto-wired.
|
|
310
368
|
- Money-safety, all fail-closed: no `402` with a null destination; `503` when the
|
|
311
369
|
backend is unreachable (the paid handler never runs); asset-integrity check
|
|
312
370
|
(backend USDC/chainId must match the SDK's native table); mode↔network check (a
|
|
313
371
|
`live` key must not resolve to a testnet); optional `expectedPayTo` pin.
|
|
314
|
-
- Exports: `
|
|
372
|
+
- Exports: `GenesisPay`, `GenesisPayConfigError`, `GenesisPayNetworkSafetyError`, and the
|
|
315
373
|
related types.
|
|
316
374
|
|
|
317
375
|
### Requirements
|
|
@@ -321,10 +379,10 @@ Workers and Bun. Remember the `await`.
|
|
|
321
379
|
|
|
322
380
|
### Unchanged
|
|
323
381
|
|
|
324
|
-
- The low-level `createPaymentGate` / `
|
|
382
|
+
- The low-level `createPaymentGate` / `genesisPaySettlement` primitives are untouched and
|
|
325
383
|
remain exported. This release is purely additive.
|
|
326
384
|
|
|
327
385
|
## 0.1.0
|
|
328
386
|
|
|
329
387
|
- Initial release: framework-agnostic x402 payment gate (`createPaymentGate`,
|
|
330
|
-
`
|
|
388
|
+
`genesisPaySettlement`).
|
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
|
|
3
|
+
Framework-agnostic x402 payment gate for sellers, powered by GenesisPay (formerly GenesisPay).
|
|
4
4
|
|
|
5
5
|
Wrap any Web-standard `(Request) => Response` handler and it becomes a paid
|
|
6
6
|
endpoint:
|
|
@@ -21,21 +21,21 @@ speaks the Fetch API.
|
|
|
21
21
|
npm install @genesis-tech/genesispay-seller
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
## Quick start (legacy `
|
|
24
|
+
## Quick start (legacy `GenesisPay` client — recommended)
|
|
25
25
|
|
|
26
|
-
The `
|
|
27
|
-
(`
|
|
26
|
+
The `GenesisPay` client configures from **just the seller API key**. The key prefix
|
|
27
|
+
(`gp_sk_test_…` / `gp_sk_live_…`) determines the mode and base URL, and the payout
|
|
28
28
|
wallet + network are resolved from the key via the GenesisPay backend and cached.
|
|
29
29
|
|
|
30
30
|
```ts
|
|
31
|
-
import {
|
|
31
|
+
import { GenesisPay } from "@genesis-tech/genesispay-seller";
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const genesispay = new GenesisPay({ apiKey: process.env.GENESISPAY_SELLER_API_KEY! });
|
|
34
34
|
|
|
35
35
|
// Human hosted checkout — the wallet is defaulted server-side from the key.
|
|
36
36
|
// `metadata` and `clientReferenceId` come back on retrieve() and on the webhook,
|
|
37
37
|
// so you never need your own table just to map a link back to a buyer:
|
|
38
|
-
const { publicId, payUrl } = await
|
|
38
|
+
const { publicId, payUrl } = await genesispay.checkout.create({
|
|
39
39
|
title: "50 credits",
|
|
40
40
|
amount: "5.00", // decimal string in `asset` — see "Amounts and assets" below
|
|
41
41
|
clientReferenceId: order.id,
|
|
@@ -45,13 +45,13 @@ const { publicId, payUrl } = await peerpay.checkout.create({
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
// Poll for settlement:
|
|
48
|
-
const session = await
|
|
48
|
+
const session = await genesispay.checkout.retrieve(publicId);
|
|
49
49
|
if (session.paid) fulfil(session.clientReferenceId);
|
|
50
|
-
// An unknown id throws
|
|
50
|
+
// An unknown id throws GenesisPayNotFoundError, not GenesisPayConfigError — so a
|
|
51
51
|
// typo'd id is distinguishable from a broken key or an unreachable backend.
|
|
52
52
|
|
|
53
53
|
// Agent x402 gate — payTo + network resolved from the key, settlement auto-wired:
|
|
54
|
-
export const GET =
|
|
54
|
+
export const GET = genesispay.gate({ amountUsdc: "0.02" }).wrap(
|
|
55
55
|
async () => Response.json({ data: "the good stuff" }),
|
|
56
56
|
);
|
|
57
57
|
```
|
|
@@ -63,7 +63,7 @@ amount is therefore a decimal dollar string — never a number:
|
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
65
|
// 5 USDC / 5 dollars:
|
|
66
|
-
await
|
|
66
|
+
await genesispay.checkout.create({ title: "Credits", amount: "5.00" });
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
Buyers can use EUR or USD in the Privy/MoonPay flow; MoonPay converts the local
|
|
@@ -77,7 +77,7 @@ breaks.
|
|
|
77
77
|
|
|
78
78
|
You may pass both, and the SDK sends both on the wire so a backend older than
|
|
79
79
|
0.6.0 still finds the field it knows. Passing **two different amounts** throws
|
|
80
|
-
`
|
|
80
|
+
`GenesisPayValidationError` before the request leaves your process — a link for
|
|
81
81
|
money you did not mean must not be created because one field quietly won.
|
|
82
82
|
|
|
83
83
|
`gate({ amountUsdc })` keeps its name deliberately: an x402 gate prices a request
|
|
@@ -89,20 +89,20 @@ Prefer a webhook over polling. Verification is one call — it checks the HMAC i
|
|
|
89
89
|
constant time and enforces a replay window on the signature timestamp:
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
|
-
import { constructEvent,
|
|
92
|
+
import { constructEvent, GenesisPaySignatureVerificationError } from "@genesis-tech/genesispay-seller";
|
|
93
93
|
|
|
94
94
|
export async function POST(request: Request) {
|
|
95
95
|
const rawBody = await request.text(); // raw — never re-serialize before verifying
|
|
96
96
|
try {
|
|
97
97
|
const event = await constructEvent(
|
|
98
98
|
rawBody,
|
|
99
|
-
request.headers.get("
|
|
100
|
-
process.env.
|
|
99
|
+
request.headers.get("GENESISPAY-SIGNATURE") ?? "",
|
|
100
|
+
process.env.GENESISPAY_WEBHOOK_SECRET!,
|
|
101
101
|
);
|
|
102
102
|
if (event.type === "payment.confirmed") await fulfil(event.data);
|
|
103
103
|
return new Response(null, { status: 204 });
|
|
104
104
|
} catch (error) {
|
|
105
|
-
if (error instanceof
|
|
105
|
+
if (error instanceof GenesisPaySignatureVerificationError) {
|
|
106
106
|
return new Response("invalid signature", { status: 400 });
|
|
107
107
|
}
|
|
108
108
|
throw error;
|
|
@@ -113,7 +113,7 @@ export async function POST(request: Request) {
|
|
|
113
113
|
`constructEvent` is **async** — unlike Stripe's synchronous equivalent. It is built
|
|
114
114
|
on WebCrypto rather than `node:crypto` so the SDK also runs on Edge, Workers and
|
|
115
115
|
Bun. It is available as a free function (a webhook route rarely has a client in
|
|
116
|
-
scope) and as `
|
|
116
|
+
scope) and as `genesispay.webhooks.constructEvent(…)`. Default replay tolerance is
|
|
117
117
|
300 s in both directions; override with `{ toleranceSeconds }`. Multiple `v1=`
|
|
118
118
|
values in the header are all checked, so you can rotate an endpoint secret without
|
|
119
119
|
dropping deliveries.
|
|
@@ -172,14 +172,14 @@ build a draft, then finalize it. Finalization freezes the billing details and
|
|
|
172
172
|
creates exactly one single-use payment link; a draft cannot be paid or emailed.
|
|
173
173
|
|
|
174
174
|
```ts
|
|
175
|
-
const customer = await
|
|
175
|
+
const customer = await genesispay.customers.create({
|
|
176
176
|
name: "Ada Lovelace",
|
|
177
177
|
email: "ada@example.com",
|
|
178
178
|
companyName: "Analytical Engines Ltd",
|
|
179
179
|
countryCode: "GB",
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
const draft = await
|
|
182
|
+
const draft = await genesispay.invoices.create({
|
|
183
183
|
customerId: customer.publicId,
|
|
184
184
|
asset: "EURC",
|
|
185
185
|
dueAt: "2026-08-31T23:59:59.999Z",
|
|
@@ -189,8 +189,8 @@ const draft = await peerpay.invoices.create({
|
|
|
189
189
|
],
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
-
const invoice = await
|
|
193
|
-
await
|
|
192
|
+
const invoice = await genesispay.invoices.finalize(draft.publicId);
|
|
193
|
+
await genesispay.invoices.send(invoice.publicId, {
|
|
194
194
|
idempotencyKey: `invoice-${invoice.publicId}-initial`,
|
|
195
195
|
});
|
|
196
196
|
|
|
@@ -212,7 +212,7 @@ customers. You do **not** need your own billing cron — renewals run on our
|
|
|
212
212
|
scheduler.
|
|
213
213
|
|
|
214
214
|
```ts
|
|
215
|
-
const plan = await
|
|
215
|
+
const plan = await genesispay.plans.create({
|
|
216
216
|
title: "Pro",
|
|
217
217
|
amountPerPeriod: "9.00",
|
|
218
218
|
periodDays: 30,
|
|
@@ -224,7 +224,7 @@ redirect(plan.checkoutUrl);
|
|
|
224
224
|
|
|
225
225
|
The customer signs one permit there; after that, renewals are charged without
|
|
226
226
|
further signatures and emit `subscription.renewed` (or `subscription.past_due`).
|
|
227
|
-
`
|
|
227
|
+
`genesispay.mandates.*` exposes the per-payer authorizations underneath —
|
|
228
228
|
`create`, `retrieve`, `charge` (per-use metering) and `revoke`.
|
|
229
229
|
|
|
230
230
|
There is deliberately **no `mandates.activate`**: activation requires the payer's
|
|
@@ -237,7 +237,7 @@ server holding your secret key.
|
|
|
237
237
|
let startingAfter: string | undefined;
|
|
238
238
|
let hasMore = true;
|
|
239
239
|
while (hasMore) {
|
|
240
|
-
const page = await
|
|
240
|
+
const page = await genesispay.mandates.list({
|
|
241
241
|
planId: plan.publicId,
|
|
242
242
|
status: "active",
|
|
243
243
|
startingAfter,
|
|
@@ -245,7 +245,7 @@ while (hasMore) {
|
|
|
245
245
|
const match = page.mandates.find(
|
|
246
246
|
(m) => m.payerWallet.toLowerCase() === wallet.toLowerCase(),
|
|
247
247
|
);
|
|
248
|
-
if (match) return
|
|
248
|
+
if (match) return genesispay.mandates.revoke(match.id);
|
|
249
249
|
startingAfter = page.mandates.at(-1)?.id;
|
|
250
250
|
hasMore = page.hasMore;
|
|
251
251
|
}
|
|
@@ -259,21 +259,123 @@ Note: failed renewals are reported as events, but there is no automatic dunning
|
|
|
259
259
|
(retry escalation, grace periods) yet — build that on
|
|
260
260
|
`subscription.past_due` / `mandate.charge_failed` for now.
|
|
261
261
|
|
|
262
|
+
### Products
|
|
263
|
+
|
|
264
|
+
A product is a catalogue entry; its payable instance is **one canonical
|
|
265
|
+
reusable checkout link**, minted idempotently — mint again (even concurrently)
|
|
266
|
+
and you get the same link with `created: false`.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const product = await genesispay.products.create({
|
|
270
|
+
name: "Market data report",
|
|
271
|
+
price: "2.00",
|
|
272
|
+
sku: "MDR-1",
|
|
273
|
+
// A redirect product creates a signed, expiring entitlement on every sale.
|
|
274
|
+
delivery: {
|
|
275
|
+
type: "redirect",
|
|
276
|
+
url: "https://your-site.example/download",
|
|
277
|
+
verifiedAt: null,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const { link, created } = await genesispay.products.createPaymentLink(
|
|
282
|
+
product.publicId,
|
|
283
|
+
);
|
|
284
|
+
// Share link.payUrl — every sale of this product settles through it.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
A confirmed purchase fires the `product.purchased` webhook. Its payload
|
|
288
|
+
carries the buyer's entitlement — `entitlement.redemptionPath` is a signed,
|
|
289
|
+
expiring redirect (~30 days) to your fulfilment URL, with `gp_*` parameters
|
|
290
|
+
(`gp_entitlement`, `gp_attempt`, `gp_simulated`, …) you can verify server-side
|
|
291
|
+
via `GET /api/v1/entitlements/verify`. Always check `simulated`: test-mode
|
|
292
|
+
purchases deliver end to end, marked, so your handler must not book them as
|
|
293
|
+
revenue.
|
|
294
|
+
|
|
295
|
+
Use the SDK rather than trusting `gp_*` query parameters from the browser:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
const verified = await genesispay.entitlements.verify(entitlementId);
|
|
299
|
+
if (
|
|
300
|
+
!verified.valid ||
|
|
301
|
+
verified.entitlement.simulated ||
|
|
302
|
+
verified.entitlement.productId !== product.publicId
|
|
303
|
+
) {
|
|
304
|
+
throw new Error("invalid entitlement");
|
|
305
|
+
}
|
|
306
|
+
// Insert verified.entitlement.publicId under a unique constraint, then fulfil.
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Product-backed API gate
|
|
310
|
+
|
|
311
|
+
For an API you operate, define the resource and its price once in the catalogue.
|
|
312
|
+
The product link—not browser input or route code—is the authority for the USDC
|
|
313
|
+
amount, Base network, destination, fee snapshot and product metadata:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
const forecast = await genesispay.products.create({
|
|
317
|
+
name: "Forecast API call",
|
|
318
|
+
price: "0.02",
|
|
319
|
+
sku: "prediction-forecast-v1",
|
|
320
|
+
delivery: {
|
|
321
|
+
type: "gate",
|
|
322
|
+
method: "POST",
|
|
323
|
+
resourceUrl: "https://predictionengine.xyz/api/v1/forecast",
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await genesispay.products.gate(forecast.publicId).prime();
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Protect the route with that product. Validate request shape before calling
|
|
331
|
+
`protect`, and make handler effects idempotent by `purchase.payment.attemptId`:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
const forecastGate = genesispay.products.gate("prod_...");
|
|
335
|
+
|
|
336
|
+
export async function POST(request: Request) {
|
|
337
|
+
const rawBody = await request.clone().text();
|
|
338
|
+
validateForecastJson(rawBody); // invalid requests never create a payment attempt
|
|
339
|
+
|
|
340
|
+
return forecastGate.protect(request, async (_request, purchase) => {
|
|
341
|
+
const cached = await readForecast(purchase.payment.attemptId);
|
|
342
|
+
if (cached) return Response.json(cached);
|
|
343
|
+
|
|
344
|
+
const result = await runForecast(rawBody);
|
|
345
|
+
await saveForecastOnce(purchase.payment.attemptId, result);
|
|
346
|
+
return Response.json(result);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
On the initial request, `protect` returns the standard `402 Payment Required`.
|
|
352
|
+
GenesisPay creates a pending attempt before that response and advertises a
|
|
353
|
+
reserved `gp_attempt` value in the x402 resource URL. On the signed retry, the
|
|
354
|
+
SDK sends only the request fingerprint to GenesisPay; it never sends forecast
|
|
355
|
+
inputs. A confirmed replay returns the same payment receipt with
|
|
356
|
+
`idempotentReplay: true`; handlers may therefore execute more than once and
|
|
357
|
+
must keep their own result cache.
|
|
358
|
+
|
|
359
|
+
`products.list({ includeArchived: true })`, `products.retrieve`,
|
|
360
|
+
`products.archive` complete the namespace. Archiving stops **new** link mints;
|
|
361
|
+
the existing canonical link stays payable. The price is copied onto the link
|
|
362
|
+
at mint — a later catalogue edit never changes what a buyer already sees.
|
|
363
|
+
|
|
262
364
|
### Testing the paid path
|
|
263
365
|
|
|
264
366
|
```ts
|
|
265
|
-
const session = await
|
|
367
|
+
const session = await genesispay.checkout.simulatePayment(publicId);
|
|
266
368
|
// session.paid === true, and the real webhooks have fired.
|
|
267
369
|
```
|
|
268
370
|
|
|
269
|
-
Test keys only — a `
|
|
371
|
+
Test keys only — a `gp_sk_live_…` key gets a 403, and on a mainnet deployment the
|
|
270
372
|
endpoint does not exist at all. The resulting attempt has `txHash: null` (no
|
|
271
373
|
transaction happened) and `simulated: true`; branch on that flag rather than on
|
|
272
374
|
the missing hash, because a *pending real* attempt has no hash either.
|
|
273
375
|
|
|
274
376
|
### Redirect parameters are not proof of payment
|
|
275
377
|
|
|
276
|
-
The hosted checkout appends `?
|
|
378
|
+
The hosted checkout appends `?genesispay_link_id=…&genesispay_status=paid` to your
|
|
277
379
|
`returnUrl`. Those parameters are a **UI hint only** — they are not signed, and a
|
|
278
380
|
payer can navigate to that URL directly without paying. Fulfil on the
|
|
279
381
|
`payment.confirmed` webhook or on `checkout.retrieve(publicId).paid`, never on the
|
|
@@ -306,14 +408,14 @@ no wallet returns a `402` `payment_not_configured` — the paid handler never ru
|
|
|
306
408
|
without a valid destination. It also refuses to advertise a wallet whose network
|
|
307
409
|
doesn't match the SDK's native-USDC table or the key mode.
|
|
308
410
|
|
|
309
|
-
The low-level `createPaymentGate` / `
|
|
411
|
+
The low-level `createPaymentGate` / `genesisPaySettlement` primitives below remain
|
|
310
412
|
available for advanced cases (custom wallet/network per gate, self-hosting).
|
|
311
413
|
|
|
312
414
|
## Quick start (Next.js route handler)
|
|
313
415
|
|
|
314
416
|
```ts
|
|
315
417
|
// app/api/premium/route.ts
|
|
316
|
-
import { createPaymentGate,
|
|
418
|
+
import { createPaymentGate, genesisPaySettlement } from "@genesis-tech/genesispay-seller";
|
|
317
419
|
|
|
318
420
|
const gate = createPaymentGate({
|
|
319
421
|
amountUsdc: "0.10",
|
|
@@ -322,9 +424,9 @@ const gate = createPaymentGate({
|
|
|
322
424
|
network: "base-sepolia", // or "base" for mainnet
|
|
323
425
|
});
|
|
324
426
|
|
|
325
|
-
const verifySettlement =
|
|
326
|
-
facilitatorBaseUrl: "https://your-
|
|
327
|
-
apiKey: process.env.
|
|
427
|
+
const verifySettlement = genesisPaySettlement({
|
|
428
|
+
facilitatorBaseUrl: "https://your-genesispay-instance.example",
|
|
429
|
+
apiKey: process.env.GENESISPAY_SELLER_KEY!, // gp_sk_...
|
|
328
430
|
});
|
|
329
431
|
|
|
330
432
|
export const GET = gate.wrap(
|
|
@@ -350,7 +452,7 @@ export const GET = gate.wrap(
|
|
|
350
452
|
|
|
351
453
|
```ts
|
|
352
454
|
import { Hono } from "hono";
|
|
353
|
-
import { createPaymentGate,
|
|
455
|
+
import { createPaymentGate, genesisPaySettlement } from "@genesis-tech/genesispay-seller";
|
|
354
456
|
|
|
355
457
|
const gate = createPaymentGate({
|
|
356
458
|
amountUsdc: "0.05",
|
|
@@ -362,9 +464,9 @@ const gate = createPaymentGate({
|
|
|
362
464
|
const gated = gate.wrap(
|
|
363
465
|
async () => Response.json({ ok: true }),
|
|
364
466
|
{
|
|
365
|
-
verifySettlement:
|
|
366
|
-
facilitatorBaseUrl: "https://your-
|
|
367
|
-
apiKey: process.env.
|
|
467
|
+
verifySettlement: genesisPaySettlement({
|
|
468
|
+
facilitatorBaseUrl: "https://your-genesispay-instance.example",
|
|
469
|
+
apiKey: process.env.GENESISPAY_SELLER_KEY!,
|
|
368
470
|
}),
|
|
369
471
|
},
|
|
370
472
|
);
|
|
@@ -392,12 +494,12 @@ The same wrapped handler drops straight into `Bun.serve({ fetch: gated })`.
|
|
|
392
494
|
| `maxTimeoutSeconds` | no | Advertised authorization validity window (default 300). |
|
|
393
495
|
|
|
394
496
|
`gate.wrap(handler, { verifySettlement })` requires a settlement hook. Use the
|
|
395
|
-
built-in `
|
|
497
|
+
built-in `genesisPaySettlement({ facilitatorBaseUrl, apiKey })`, which POSTs the
|
|
396
498
|
payment to GenesisPay's facilitator (`/api/v1/facilitator/settle`). GenesisPay
|
|
397
499
|
broadcasts the EIP-3009 authorization on-chain, verifies the USDC transfer,
|
|
398
500
|
and returns the receipt. `facilitatorBaseUrl` is optional and defaults to the
|
|
399
501
|
public development facilitator (`DEFAULT_FACILITATOR_BASE_URL`,
|
|
400
|
-
`https://
|
|
502
|
+
`https://dev.genesispay.finance`), so you can omit it in dev —
|
|
401
503
|
set it explicitly for production. Or supply your own hook:
|
|
402
504
|
|
|
403
505
|
```ts
|
|
@@ -425,5 +527,5 @@ const verifySettlement: VerifySettlement = async ({ payment, requirement }) => {
|
|
|
425
527
|
- The gate performs structural validation only (amount, destination, network,
|
|
426
528
|
validity window). Actual money movement and on-chain verification happen in
|
|
427
529
|
your `verifySettlement` hook.
|
|
428
|
-
- Get a seller API key (`
|
|
530
|
+
- Get a seller API key (`gp_sk_...`) from your GenesisPay dashboard under
|
|
429
531
|
Developers.
|