@genesis-tech/genesispay-seller 0.8.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 +330 -0
- package/LICENSE +21 -0
- package/README.md +429 -0
- package/dist/client.d.ts +275 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +541 -0
- package/dist/client.js.map +1 -0
- package/dist/customers.d.ts +41 -0
- package/dist/customers.d.ts.map +1 -0
- package/dist/customers.js +76 -0
- package/dist/customers.js.map +1 -0
- package/dist/errors.d.ts +63 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +162 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/invoices.d.ts +80 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +133 -0
- package/dist/invoices.js.map +1 -0
- package/dist/mandate-gate.d.ts +49 -0
- package/dist/mandate-gate.d.ts.map +1 -0
- package/dist/mandate-gate.js +109 -0
- package/dist/mandate-gate.js.map +1 -0
- package/dist/mandates.d.ts +172 -0
- package/dist/mandates.d.ts.map +1 -0
- package/dist/mandates.js +213 -0
- package/dist/mandates.js.map +1 -0
- package/dist/networks.d.ts +10 -0
- package/dist/networks.d.ts.map +1 -0
- package/dist/networks.js +22 -0
- package/dist/networks.js.map +1 -0
- package/dist/payment-gate.d.ts +67 -0
- package/dist/payment-gate.d.ts.map +1 -0
- package/dist/payment-gate.js +106 -0
- package/dist/payment-gate.js.map +1 -0
- package/dist/peerpay-settlement.d.ts +27 -0
- package/dist/peerpay-settlement.d.ts.map +1 -0
- package/dist/peerpay-settlement.js +100 -0
- package/dist/peerpay-settlement.js.map +1 -0
- package/dist/plans.d.ts +68 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +127 -0
- package/dist/plans.js.map +1 -0
- package/dist/resource.d.ts +47 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +71 -0
- package/dist/resource.js.map +1 -0
- package/dist/usdc-amount.d.ts +7 -0
- package/dist/usdc-amount.d.ts.map +1 -0
- package/dist/usdc-amount.js +21 -0
- package/dist/usdc-amount.js.map +1 -0
- package/dist/webhooks.d.ts +277 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +223 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# @genesis-tech/genesispay-seller
|
|
2
|
+
|
|
3
|
+
Framework-agnostic x402 payment gate for sellers, powered by GenesisPay (formerly PeerDirect).
|
|
4
|
+
|
|
5
|
+
Wrap any Web-standard `(Request) => Response` handler and it becomes a paid
|
|
6
|
+
endpoint:
|
|
7
|
+
|
|
8
|
+
- Requests without payment get `402 Payment Required` with an x402 V2
|
|
9
|
+
`PAYMENT-REQUIRED` header (and JSON body) describing the USDC payment.
|
|
10
|
+
- Requests carrying a `PAYMENT-SIGNATURE` header are structurally validated
|
|
11
|
+
against the requirement, settled via your `verifySettlement` hook, and — on
|
|
12
|
+
success — your handler runs and its response carries a `PAYMENT-RESPONSE`
|
|
13
|
+
header with the settlement receipt (tx hash, payer, amount).
|
|
14
|
+
|
|
15
|
+
Works with Next.js route handlers, Hono, Bun.serve, and anything else that
|
|
16
|
+
speaks the Fetch API.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @genesis-tech/genesispay-seller
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start (legacy `PeerPay` client — recommended)
|
|
25
|
+
|
|
26
|
+
The `PeerPay` client configures from **just the seller API key**. The key prefix
|
|
27
|
+
(`pp_sk_test_…` / `pp_sk_live_…`) determines the mode and base URL, and the payout
|
|
28
|
+
wallet + network are resolved from the key via the GenesisPay backend and cached.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { PeerPay } from "@genesis-tech/genesispay-seller";
|
|
32
|
+
|
|
33
|
+
const peerpay = new PeerPay({ apiKey: process.env.PEERPAY_SELLER_API_KEY! });
|
|
34
|
+
|
|
35
|
+
// Human hosted checkout — the wallet is defaulted server-side from the key.
|
|
36
|
+
// `metadata` and `clientReferenceId` come back on retrieve() and on the webhook,
|
|
37
|
+
// so you never need your own table just to map a link back to a buyer:
|
|
38
|
+
const { publicId, payUrl } = await peerpay.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
|
+
});
|
|
46
|
+
|
|
47
|
+
// Poll for settlement:
|
|
48
|
+
const session = await peerpay.checkout.retrieve(publicId);
|
|
49
|
+
if (session.paid) fulfil(session.clientReferenceId);
|
|
50
|
+
// An unknown id throws PeerPayNotFoundError, not PeerPayConfigError — so a
|
|
51
|
+
// typo'd id is distinguishable from a broken key or an unreachable backend.
|
|
52
|
+
|
|
53
|
+
// Agent x402 gate — payTo + network resolved from the key, settlement auto-wired:
|
|
54
|
+
export const GET = peerpay.gate({ amountUsdc: "0.02" }).wrap(
|
|
55
|
+
async () => Response.json({ data: "the good stuff" }),
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Amounts and settlement
|
|
60
|
+
|
|
61
|
+
During the beta, every new hosted checkout link settles in **USDC on Base**. An
|
|
62
|
+
amount is therefore a decimal dollar string — never a number:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// 5 USDC / 5 dollars:
|
|
66
|
+
await peerpay.checkout.create({ title: "Credits", amount: "5.00" });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Buyers can use EUR or USD in the Privy/MoonPay flow; MoonPay converts the local
|
|
70
|
+
fiat amount to USDC before settlement. The provider shows the final live quote
|
|
71
|
+
and fees. New `asset: "EURC"` requests are rejected during the beta; historical
|
|
72
|
+
EURC links remain readable.
|
|
73
|
+
|
|
74
|
+
`amountUsdc` is the **deprecated** pre-0.6.0 name for `amount`. It still works
|
|
75
|
+
everywhere `amount` does, on input and on output, so no existing integration
|
|
76
|
+
breaks.
|
|
77
|
+
|
|
78
|
+
You may pass both, and the SDK sends both on the wire so a backend older than
|
|
79
|
+
0.6.0 still finds the field it knows. Passing **two different amounts** throws
|
|
80
|
+
`PeerPayValidationError` before the request leaves your process — a link for
|
|
81
|
+
money you did not mean must not be created because one field quietly won.
|
|
82
|
+
|
|
83
|
+
`gate({ amountUsdc })` keeps its name deliberately: an x402 gate prices a request
|
|
84
|
+
in USDC on Base, so there the currency really is part of the field.
|
|
85
|
+
|
|
86
|
+
### Webhooks
|
|
87
|
+
|
|
88
|
+
Prefer a webhook over polling. Verification is one call — it checks the HMAC in
|
|
89
|
+
constant time and enforces a replay window on the signature timestamp:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { constructEvent, PeerPaySignatureVerificationError } from "@genesis-tech/genesispay-seller";
|
|
93
|
+
|
|
94
|
+
export async function POST(request: Request) {
|
|
95
|
+
const rawBody = await request.text(); // raw — never re-serialize before verifying
|
|
96
|
+
try {
|
|
97
|
+
const event = await constructEvent(
|
|
98
|
+
rawBody,
|
|
99
|
+
request.headers.get("PEERPAY-SIGNATURE") ?? "",
|
|
100
|
+
process.env.PEERPAY_WEBHOOK_SECRET!,
|
|
101
|
+
);
|
|
102
|
+
if (event.type === "payment.confirmed") await fulfil(event.data);
|
|
103
|
+
return new Response(null, { status: 204 });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof PeerPaySignatureVerificationError) {
|
|
106
|
+
return new Response("invalid signature", { status: 400 });
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`constructEvent` is **async** — unlike Stripe's synchronous equivalent. It is built
|
|
114
|
+
on WebCrypto rather than `node:crypto` so the SDK also runs on Edge, Workers and
|
|
115
|
+
Bun. It is available as a free function (a webhook route rarely has a client in
|
|
116
|
+
scope) and as `peerpay.webhooks.constructEvent(…)`. Default replay tolerance is
|
|
117
|
+
300 s in both directions; override with `{ toleranceSeconds }`. Multiple `v1=`
|
|
118
|
+
values in the header are all checked, so you can rotate an endpoint secret without
|
|
119
|
+
dropping deliveries.
|
|
120
|
+
|
|
121
|
+
**Read `event.data.link.asset` before you book the money.** The payment payload
|
|
122
|
+
carries `amount` (a decimal string) and the `asset` it is denominated in — euros
|
|
123
|
+
on an EURC link, dollars on a USDC one. `amountUsdc` is the deprecated alias carrying the identical value;
|
|
124
|
+
on an EURC link its name is simply wrong, which is why `amount` replaced it.
|
|
125
|
+
`amountUsdcMinor` keeps its name and its meaning: the integer minor units of that
|
|
126
|
+
same amount.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
if (event.type === "payment.confirmed") {
|
|
130
|
+
const { asset, amount, amountUsdcMinor } = event.data.link;
|
|
131
|
+
// `asset` is typed optional: a delivery enqueued before 0.7.0 is retried
|
|
132
|
+
// verbatim and arrives without one. Do not default it — guessing "USDC" on a
|
|
133
|
+
// euro payment is the bug this field removes. A missing `asset` is genuinely
|
|
134
|
+
// unbookable; that is why it, and not `amount`, is the guard.
|
|
135
|
+
if (!asset) return ack("asset missing — resolving via checkout.retrieve");
|
|
136
|
+
await recordRevenue({ currency: asset, amount, minorUnits: BigInt(amountUsdcMinor) });
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`event.data.attempt.chainId` is the chain `attempt.txHash` is on — read the two
|
|
141
|
+
together, since the same hash resolves to nothing on the wrong chain. It is on
|
|
142
|
+
the *attempt*, not the link, because a pay-by-bank settlement mints on whatever
|
|
143
|
+
chain the provider uses, which need not be the link's.
|
|
144
|
+
|
|
145
|
+
A missing `chainId` is **not** the same problem as a missing `asset` — the amount
|
|
146
|
+
and currency are still fully determined — but what to do about it depends on you.
|
|
147
|
+
If your endpoint only ever receives one chain, book the payment and skip the
|
|
148
|
+
explorer link. If it can receive more than one, resolve through
|
|
149
|
+
`checkout.retrieve` before booking: an amount whose chain you cannot name is
|
|
150
|
+
exactly what this field exists to prevent you from mis-booking.
|
|
151
|
+
|
|
152
|
+
Either way, do not default it, and mind the polarity: `undefined !== 8453` is
|
|
153
|
+
`true`, so `if (chainId !== 8453)` reads a missing chain as testnet and its
|
|
154
|
+
mirror reads it as mainnet. Branch on presence first. And do not assume the set
|
|
155
|
+
is Base-only — a pay-by-bank settlement reports the provider's chain, which can
|
|
156
|
+
be Gnosis, Polygon or Ethereum.
|
|
157
|
+
|
|
158
|
+
**One settlement can reach you twice — handle both cases separately.**
|
|
159
|
+
|
|
160
|
+
- *Retries*: a delivery is attempted up to three times on a non-2xx response, and
|
|
161
|
+
`event.id` is stable across those attempts. Dedupe on `event.id` before fulfilling.
|
|
162
|
+
- *The event pair*: a **single-use** link emits both `payment.confirmed` and
|
|
163
|
+
`link.paid` for the same settlement. These are two distinct deliveries with two
|
|
164
|
+
**different** `event.id`s, so `event.id` will not collapse them — branch on
|
|
165
|
+
`event.type` and fulfil on one of them. Reusable links emit only
|
|
166
|
+
`payment.confirmed`.
|
|
167
|
+
|
|
168
|
+
### Invoices
|
|
169
|
+
|
|
170
|
+
Invoices are one-off commercial billing documents. Create or reuse a customer,
|
|
171
|
+
build a draft, then finalize it. Finalization freezes the billing details and
|
|
172
|
+
creates exactly one single-use payment link; a draft cannot be paid or emailed.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const customer = await peerpay.customers.create({
|
|
176
|
+
name: "Ada Lovelace",
|
|
177
|
+
email: "ada@example.com",
|
|
178
|
+
companyName: "Analytical Engines Ltd",
|
|
179
|
+
countryCode: "GB",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const draft = await peerpay.invoices.create({
|
|
183
|
+
customerId: customer.publicId,
|
|
184
|
+
asset: "EURC",
|
|
185
|
+
dueAt: "2026-08-31T23:59:59.999Z",
|
|
186
|
+
taxBps: 2_300, // 23%; manual rate, not automated tax advice
|
|
187
|
+
lineItems: [
|
|
188
|
+
{ description: "Consulting", quantity: 2, unitAmount: "450.00" },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const invoice = await peerpay.invoices.finalize(draft.publicId);
|
|
193
|
+
await peerpay.invoices.send(invoice.publicId, {
|
|
194
|
+
idempotencyKey: `invoice-${invoice.publicId}-initial`,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
invoice.hostedInvoiceUrl; // customer-facing document
|
|
198
|
+
invoice.pdfUrl; // printable PDF
|
|
199
|
+
invoice.payment.payUrl; // canonical GenesisPay checkout
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
All invoice money fields ending in `Minor` are integer strings. `paid` is a
|
|
203
|
+
read-only projection of a confirmed, non-simulated GenesisPay payment; it cannot
|
|
204
|
+
be set through the SDK. Finalized invoices cannot be edited. Use
|
|
205
|
+
`invoices.void()` to cancel collection or `invoices.markUncollectible()` to write
|
|
206
|
+
off an unpaid invoice.
|
|
207
|
+
|
|
208
|
+
### Subscriptions
|
|
209
|
+
|
|
210
|
+
Plans are the reusable template you create once and hand to any number of
|
|
211
|
+
customers. You do **not** need your own billing cron — renewals run on our
|
|
212
|
+
scheduler.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const plan = await peerpay.plans.create({
|
|
216
|
+
title: "Pro",
|
|
217
|
+
amountPerPeriod: "9.00",
|
|
218
|
+
periodDays: 30,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Send the customer to the hosted subscription checkout:
|
|
222
|
+
redirect(plan.checkoutUrl);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The customer signs one permit there; after that, renewals are charged without
|
|
226
|
+
further signatures and emit `subscription.renewed` (or `subscription.past_due`).
|
|
227
|
+
`peerpay.mandates.*` exposes the per-payer authorizations underneath —
|
|
228
|
+
`create`, `retrieve`, `charge` (per-use metering) and `revoke`.
|
|
229
|
+
|
|
230
|
+
There is deliberately **no `mandates.activate`**: activation requires the payer's
|
|
231
|
+
own signature over the permit, so it belongs in the payer's frontend, not in a
|
|
232
|
+
server holding your secret key.
|
|
233
|
+
|
|
234
|
+
**Cancelling a subscription** — list the plan's subscribers, find the payer, revoke:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
let startingAfter: string | undefined;
|
|
238
|
+
let hasMore = true;
|
|
239
|
+
while (hasMore) {
|
|
240
|
+
const page = await peerpay.mandates.list({
|
|
241
|
+
planId: plan.publicId,
|
|
242
|
+
status: "active",
|
|
243
|
+
startingAfter,
|
|
244
|
+
});
|
|
245
|
+
const match = page.mandates.find(
|
|
246
|
+
(m) => m.payerWallet.toLowerCase() === wallet.toLowerCase(),
|
|
247
|
+
);
|
|
248
|
+
if (match) return peerpay.mandates.revoke(match.id);
|
|
249
|
+
startingAfter = page.mandates.at(-1)?.id;
|
|
250
|
+
hasMore = page.hasMore;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Pagination is keyset-based: `startingAfter` is the id of the last mandate on the
|
|
255
|
+
previous page. Revoking is idempotent — a second call changes nothing and emits
|
|
256
|
+
no second `mandate.revoked`.
|
|
257
|
+
|
|
258
|
+
Note: failed renewals are reported as events, but there is no automatic dunning
|
|
259
|
+
(retry escalation, grace periods) yet — build that on
|
|
260
|
+
`subscription.past_due` / `mandate.charge_failed` for now.
|
|
261
|
+
|
|
262
|
+
### Testing the paid path
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const session = await peerpay.checkout.simulatePayment(publicId);
|
|
266
|
+
// session.paid === true, and the real webhooks have fired.
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Test keys only — a `pp_sk_live_…` key gets a 403, and on a mainnet deployment the
|
|
270
|
+
endpoint does not exist at all. The resulting attempt has `txHash: null` (no
|
|
271
|
+
transaction happened) and `simulated: true`; branch on that flag rather than on
|
|
272
|
+
the missing hash, because a *pending real* attempt has no hash either.
|
|
273
|
+
|
|
274
|
+
### Redirect parameters are not proof of payment
|
|
275
|
+
|
|
276
|
+
The hosted checkout appends `?peerpay_link_id=…&peerpay_status=paid` to your
|
|
277
|
+
`returnUrl`. Those parameters are a **UI hint only** — they are not signed, and a
|
|
278
|
+
payer can navigate to that URL directly without paying. Fulfil on the
|
|
279
|
+
`payment.confirmed` webhook or on `checkout.retrieve(publicId).paid`, never on the
|
|
280
|
+
query string.
|
|
281
|
+
|
|
282
|
+
### Limits
|
|
283
|
+
|
|
284
|
+
| Field | Limit |
|
|
285
|
+
|---|---|
|
|
286
|
+
| `metadata` | 20 keys; keys ≤ 40 chars; values must be strings, ≤ 500 chars; ≤ 4096 bytes serialized |
|
|
287
|
+
| `clientReferenceId` | ≤ 200 chars |
|
|
288
|
+
| `returnUrl` / `cancelUrl` | ≤ 2048 chars, `https` only (`http` allowed for localhost) |
|
|
289
|
+
|
|
290
|
+
Exceeding any of these fails the `checkout.create` call server-side with a 422.
|
|
291
|
+
|
|
292
|
+
### Note on `paid` for reusable links
|
|
293
|
+
|
|
294
|
+
`session.paid` is derived from `confirmedPaymentCount > 0`. For a `reusable` link
|
|
295
|
+
that counter only ever grows, so `paid` stays `true` from the first payment
|
|
296
|
+
onward — it answers "has this link ever been paid", not "has *this* buyer paid".
|
|
297
|
+
For per-buyer fulfilment on a reusable link, use a webhook, or track
|
|
298
|
+
`confirmedPaymentCount` as a delta.
|
|
299
|
+
|
|
300
|
+
Options: `baseUrl` (override the mode default — https, http only for localhost;
|
|
301
|
+
both modes default to the GenesisPay facilitator, so it is optional), `configTtlMs`
|
|
302
|
+
(seller-config cache TTL, default 5 min), `expectedPayTo` (recommended for `live`
|
|
303
|
+
keys — a local pin that fail-closes if the resolved wallet ever differs), `fetchFn`.
|
|
304
|
+
The client fails **closed**: an unreachable backend returns `503` and a seller with
|
|
305
|
+
no wallet returns a `402` `payment_not_configured` — the paid handler never runs
|
|
306
|
+
without a valid destination. It also refuses to advertise a wallet whose network
|
|
307
|
+
doesn't match the SDK's native-USDC table or the key mode.
|
|
308
|
+
|
|
309
|
+
The low-level `createPaymentGate` / `peerPaySettlement` primitives below remain
|
|
310
|
+
available for advanced cases (custom wallet/network per gate, self-hosting).
|
|
311
|
+
|
|
312
|
+
## Quick start (Next.js route handler)
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// app/api/premium/route.ts
|
|
316
|
+
import { createPaymentGate, peerPaySettlement } from "@genesis-tech/genesispay-seller";
|
|
317
|
+
|
|
318
|
+
const gate = createPaymentGate({
|
|
319
|
+
amountUsdc: "0.10",
|
|
320
|
+
payTo: "0xYourWalletAddress",
|
|
321
|
+
description: "Premium market data",
|
|
322
|
+
network: "base-sepolia", // or "base" for mainnet
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const verifySettlement = peerPaySettlement({
|
|
326
|
+
facilitatorBaseUrl: "https://your-peerpay-instance.example",
|
|
327
|
+
apiKey: process.env.PEERPAY_SELLER_KEY!, // pp_sk_...
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
export const GET = gate.wrap(
|
|
331
|
+
async () => Response.json({ data: "the good stuff" }),
|
|
332
|
+
{ verifySettlement },
|
|
333
|
+
);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Next.js route context (`{ params }`) is passed through to your handler
|
|
337
|
+
untouched, so dynamic routes work as usual:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
export const GET = gate.wrap(
|
|
341
|
+
async (request, { params }: { params: Promise<{ id: string }> }) => {
|
|
342
|
+
const { id } = await params;
|
|
343
|
+
return Response.json({ id });
|
|
344
|
+
},
|
|
345
|
+
{ verifySettlement },
|
|
346
|
+
);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Quick start (Hono)
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { Hono } from "hono";
|
|
353
|
+
import { createPaymentGate, peerPaySettlement } from "@genesis-tech/genesispay-seller";
|
|
354
|
+
|
|
355
|
+
const gate = createPaymentGate({
|
|
356
|
+
amountUsdc: "0.05",
|
|
357
|
+
payTo: "0xYourWalletAddress",
|
|
358
|
+
description: "Paid API call",
|
|
359
|
+
network: "base-sepolia",
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const gated = gate.wrap(
|
|
363
|
+
async () => Response.json({ ok: true }),
|
|
364
|
+
{
|
|
365
|
+
verifySettlement: peerPaySettlement({
|
|
366
|
+
facilitatorBaseUrl: "https://your-peerpay-instance.example",
|
|
367
|
+
apiKey: process.env.PEERPAY_SELLER_KEY!,
|
|
368
|
+
}),
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const app = new Hono();
|
|
373
|
+
app.get("/premium", (c) => gated(c.req.raw));
|
|
374
|
+
|
|
375
|
+
export default app;
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The same wrapped handler drops straight into `Bun.serve({ fetch: gated })`.
|
|
379
|
+
|
|
380
|
+
## Configuration
|
|
381
|
+
|
|
382
|
+
`createPaymentGate(config)`:
|
|
383
|
+
|
|
384
|
+
| Option | Required | Description |
|
|
385
|
+
| --- | --- | --- |
|
|
386
|
+
| `amountUsdc` | yes | Decimal USDC amount, e.g. `"0.10"` (max 6 decimals). |
|
|
387
|
+
| `payTo` | yes | EVM wallet address that receives the USDC. |
|
|
388
|
+
| `description` | no | Shown to payers in the payment requirement. |
|
|
389
|
+
| `network` | no | `"base-sepolia"` (default) or `"base"`. |
|
|
390
|
+
| `resource` | no | Canonical resource URL. Defaults to the request URL (query stripped). |
|
|
391
|
+
| `mimeType` | no | MIME type of the paid resource (default `application/json`). |
|
|
392
|
+
| `maxTimeoutSeconds` | no | Advertised authorization validity window (default 300). |
|
|
393
|
+
|
|
394
|
+
`gate.wrap(handler, { verifySettlement })` requires a settlement hook. Use the
|
|
395
|
+
built-in `peerPaySettlement({ facilitatorBaseUrl, apiKey })`, which POSTs the
|
|
396
|
+
payment to GenesisPay's facilitator (`/api/v1/facilitator/settle`). GenesisPay
|
|
397
|
+
broadcasts the EIP-3009 authorization on-chain, verifies the USDC transfer,
|
|
398
|
+
and returns the receipt. `facilitatorBaseUrl` is optional and defaults to the
|
|
399
|
+
public development facilitator (`DEFAULT_FACILITATOR_BASE_URL`,
|
|
400
|
+
`https://peerpay-app-development.up.railway.app`), so you can omit it in dev —
|
|
401
|
+
set it explicitly for production. Or supply your own hook:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import type { VerifySettlement } from "@genesis-tech/genesispay-seller";
|
|
405
|
+
|
|
406
|
+
const verifySettlement: VerifySettlement = async ({ payment, requirement }) => {
|
|
407
|
+
// settle + verify however you like, then:
|
|
408
|
+
return {
|
|
409
|
+
ok: true,
|
|
410
|
+
settlement: {
|
|
411
|
+
success: true,
|
|
412
|
+
transaction: "0x...",
|
|
413
|
+
network: requirement.network,
|
|
414
|
+
amount: requirement.maxAmountRequired,
|
|
415
|
+
payer: payment.payload.authorization.from,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Notes
|
|
422
|
+
|
|
423
|
+
- Amounts are handled as integer USDC minor units (6 decimals) internally —
|
|
424
|
+
never floats.
|
|
425
|
+
- The gate performs structural validation only (amount, destination, network,
|
|
426
|
+
validity window). Actual money movement and on-chain verification happen in
|
|
427
|
+
your `verifySettlement` hook.
|
|
428
|
+
- Get a seller API key (`pp_sk_...`) from your GenesisPay dashboard under
|
|
429
|
+
Developers.
|