@profullstack/x402-gateway 0.2.2 → 0.3.1
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/README.md +4 -1
- package/index.d.ts +19 -1
- package/package.json +1 -1
- package/src/index.js +112 -21
- package/src/page.js +15 -4
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Sell crawl access to AI training crawlers, by the day, over [x402](https://x402.
|
|
|
4
4
|
|
|
5
5
|
People read your site free. So do search engines and the retrieval crawlers behind AI answers, because they send readers back. A crawler that copies pages into a training corpus sends nobody back, so it pays: every page answers `402 Payment Required` with an x402 offer, paying the offer returns a signed pass, and the pass opens the site for a day.
|
|
6
6
|
|
|
7
|
+
A crawler that wants longer buys more days in one payment. `?days=7` on the sales page quotes seven days at the daily price, and the days a proof buys are read off the value it authorizes, so paying seven times the price — however it was asked for — returns a pass that expires seven days out. `maxDays` caps how many one proof can buy.
|
|
8
|
+
|
|
7
9
|
One middleware. No database. Runs in Node, Bun and at the edge.
|
|
8
10
|
|
|
9
11
|
```
|
|
@@ -82,7 +84,8 @@ export const GET = robotsRoute(gateway, { disallow: ['/login', '/api/'] });
|
|
|
82
84
|
| `coinpay.apiKey` | | a **scoped** CoinPay key (`cp_live_…`, from the business's API Keys tab) with `payments:create`. The legacy business key is refused by CoinPay's x402 routes. |
|
|
83
85
|
| `payTo` | | EVM address that receives the USDC, on Base, Polygon and Ethereum alike |
|
|
84
86
|
| `priceCents` | `100` | |
|
|
85
|
-
| `passMinutes` | `1440` | a day |
|
|
87
|
+
| `passMinutes` | `1440` | a day: the term one price buys |
|
|
88
|
+
| `maxDays` | `30` | the most terms one proof may buy at once |
|
|
86
89
|
| `header` | `x-crawl-pass` | where the pass goes; `Authorization: Bearer` works too |
|
|
87
90
|
| `path` | `/crawl` | the sales page |
|
|
88
91
|
| `openPaths` | `[]` | extra paths a refused crawler may read (`robots.txt`, the sales page, `security.txt` and `.well-known/` always are) |
|
package/index.d.ts
CHANGED
|
@@ -7,7 +7,12 @@ export interface Sale {
|
|
|
7
7
|
token: string;
|
|
8
8
|
expiresAt: string;
|
|
9
9
|
userAgent: string;
|
|
10
|
+
/** Per term (`passMinutes`). */
|
|
10
11
|
priceCents: number;
|
|
12
|
+
/** Terms this proof bought. */
|
|
13
|
+
days: number;
|
|
14
|
+
/** `priceCents * days`. */
|
|
15
|
+
totalCents: number;
|
|
11
16
|
currency: string;
|
|
12
17
|
}
|
|
13
18
|
|
|
@@ -15,8 +20,14 @@ export interface PageContext {
|
|
|
15
20
|
siteName: string;
|
|
16
21
|
siteUrl: string;
|
|
17
22
|
buyUrl: string;
|
|
23
|
+
/** Per day, e.g. "1.00 USD". */
|
|
18
24
|
price: string;
|
|
19
25
|
minutes: number;
|
|
26
|
+
/** Days this page's offer quotes (`?days=`), 1 by default. */
|
|
27
|
+
days: number;
|
|
28
|
+
/** `price` times `days`. */
|
|
29
|
+
total: string;
|
|
30
|
+
maxDays: number;
|
|
20
31
|
header: string;
|
|
21
32
|
enabled: boolean;
|
|
22
33
|
offer: Offer;
|
|
@@ -37,8 +48,10 @@ export interface GatewayOptions {
|
|
|
37
48
|
/** Default 100 ($1). */
|
|
38
49
|
priceCents?: number;
|
|
39
50
|
currency?: string;
|
|
40
|
-
/** What
|
|
51
|
+
/** What one price buys. Default 1440 (a day). */
|
|
41
52
|
passMinutes?: number;
|
|
53
|
+
/** The most terms one proof may buy at once (`?days=` and paid multiples are clamped to it). Default 30. */
|
|
54
|
+
maxDays?: number;
|
|
42
55
|
/** Request header the pass is presented in. Default 'x-crawl-pass'. */
|
|
43
56
|
header?: string;
|
|
44
57
|
/** The sales page. Default '/crawl'. */
|
|
@@ -59,6 +72,7 @@ export interface GatewayOptions {
|
|
|
59
72
|
secret?: string;
|
|
60
73
|
page?: (ctx: PageContext) => string;
|
|
61
74
|
contact?: string;
|
|
75
|
+
/** Awaited before the buyer's receipt is sent, and its errors are swallowed: a sale is recorded, and a recording failure never costs the buyer the pass. */
|
|
62
76
|
onSale?: (sale: Sale) => void | Promise<void>;
|
|
63
77
|
fetch?: typeof fetch;
|
|
64
78
|
}
|
|
@@ -133,6 +147,10 @@ export const X402_METHODS: typeof METHODS;
|
|
|
133
147
|
export function buildOffer(args: { payTo: string; priceCents: number; resource: string; description?: string; maxTimeoutSeconds?: number; methods?: typeof METHODS }): Offer;
|
|
134
148
|
export function decodePayment(header: string | null | undefined): Record<string, unknown> | null;
|
|
135
149
|
export function expectedFor(payment: unknown, offer: Offer): { amount: string; resource: string; payTo: string; asset: string } | null;
|
|
150
|
+
/** The value a proof authorizes, in the token's smallest unit, or null. */
|
|
151
|
+
export function paidValueOf(payment: unknown): bigint | null;
|
|
152
|
+
/** How many terms `value` buys at `unit` per term: a whole number in [1, maxDays], or 0. */
|
|
153
|
+
export function daysPaid(value: bigint | null, unit: string | number | bigint, maxDays: number): number;
|
|
136
154
|
export function verifyAndSettle(
|
|
137
155
|
payment: unknown,
|
|
138
156
|
expected: { amount: string; resource: string; payTo: string; asset: string },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/x402-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sell crawl access to AI training crawlers by the day over x402, settled by CoinPay. One middleware: 402 with an offer, a sales page with CLI instructions, signed passes, and a robots.txt that keeps search crawlers welcome.",
|
|
6
6
|
"keywords": [
|
package/src/index.js
CHANGED
|
@@ -29,7 +29,9 @@ export { buildOffer, decodePayment, expectedFor, METHODS, verifyAndSettle } from
|
|
|
29
29
|
* or as an HTML sales page if it asked for HTML -- on every path but the few it
|
|
30
30
|
* needs to read to comply. Paying the offer, at the sales page or on any 402'd
|
|
31
31
|
* URL, returns a signed pass good for `passMinutes`, presented in `header` on
|
|
32
|
-
* every request after that.
|
|
32
|
+
* every request after that. A crawler that wants longer buys more days at
|
|
33
|
+
* once: `?days=N` on the sales page quotes N terms, and a proof for N times
|
|
34
|
+
* the price — however it was asked for — buys a pass that lasts N terms.
|
|
33
35
|
*
|
|
34
36
|
* Framework-agnostic: `handle(request)` takes a Fetch `Request` and resolves to
|
|
35
37
|
* a `Response` to send, or null to let the request through. The adapters in
|
|
@@ -42,7 +44,8 @@ export { buildOffer, decodePayment, expectedFor, METHODS, verifyAndSettle } from
|
|
|
42
44
|
* @param {string} [options.payTo] EVM address that receives the USDC
|
|
43
45
|
* @param {number} [options.priceCents=100]
|
|
44
46
|
* @param {string} [options.currency='USD']
|
|
45
|
-
* @param {number} [options.passMinutes=1440] a day
|
|
47
|
+
* @param {number} [options.passMinutes=1440] a day: the term one payment buys
|
|
48
|
+
* @param {number} [options.maxDays=30] the most terms one proof may buy at once
|
|
46
49
|
* @param {string} [options.header='x-crawl-pass']
|
|
47
50
|
* @param {string} [options.path='/crawl'] the sales page
|
|
48
51
|
* @param {string[]} [options.openPaths] extra paths a refused crawler may read
|
|
@@ -53,7 +56,7 @@ export { buildOffer, decodePayment, expectedFor, METHODS, verifyAndSettle } from
|
|
|
53
56
|
* @param {string} [options.secret] pass signing secret; defaults to the CoinPay key
|
|
54
57
|
* @param {(ctx: object) => string} [options.page] custom sales page renderer
|
|
55
58
|
* @param {string} [options.contact] mailto: or URL for bulk deals
|
|
56
|
-
* @param {(sale: object) => void|Promise<void>} [options.onSale] accounting hook
|
|
59
|
+
* @param {(sale: object) => void|Promise<void>} [options.onSale] accounting hook; awaited before the receipt goes out, and its errors are swallowed
|
|
57
60
|
* @param {typeof fetch} [options.fetch] for tests
|
|
58
61
|
*/
|
|
59
62
|
export function createGateway(options = {}) {
|
|
@@ -65,22 +68,45 @@ export function createGateway(options = {}) {
|
|
|
65
68
|
const denied = compileCidrs(o.denyCidrs);
|
|
66
69
|
const isOpen = (path) => openPaths.some((p) => (p.endsWith('/') ? path.startsWith(p) : path === p));
|
|
67
70
|
|
|
68
|
-
const
|
|
71
|
+
const money = (cents) => `${(cents / 100).toFixed(2)} ${o.currency}`;
|
|
72
|
+
const price = money(o.priceCents);
|
|
69
73
|
const buyUrl = `${o.siteUrl}${o.path}`;
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
/**
|
|
76
|
+
* How many terms a request is asking to buy: `?days=N`, clamped to
|
|
77
|
+
* [1, maxDays]. Anything unparseable is one day, which is what the offer
|
|
78
|
+
* always meant before there was a way to ask for more.
|
|
79
|
+
*/
|
|
80
|
+
const daysFrom = (request) => {
|
|
81
|
+
const raw = new URL(request.url).searchParams.get('days');
|
|
82
|
+
const n = Number.parseInt(raw ?? '', 10);
|
|
83
|
+
if (!Number.isFinite(n) || n < 1) return 1;
|
|
84
|
+
return Math.min(n, o.maxDays);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** The offer for `days` terms: the same entries, `days` times the price. */
|
|
88
|
+
const offer = (days = 1) =>
|
|
72
89
|
enabled
|
|
73
90
|
? buildOffer({
|
|
74
91
|
payTo: o.payTo,
|
|
75
|
-
priceCents: o.priceCents,
|
|
92
|
+
priceCents: o.priceCents * days,
|
|
76
93
|
resource: buyUrl,
|
|
77
|
-
description: `${o.passMinutes} minutes of crawl access to ${o.siteUrl}`,
|
|
94
|
+
description: `${days * o.passMinutes} minutes of crawl access to ${o.siteUrl}${days > 1 ? ` (${days} × ${o.passMinutes})` : ''}`,
|
|
78
95
|
})
|
|
79
96
|
: { x402Version: 2, accepts: [] };
|
|
80
97
|
|
|
81
|
-
const receipt = (extra = {}) => ({
|
|
82
|
-
...offer(),
|
|
83
|
-
pass: {
|
|
98
|
+
const receipt = (days = 1, extra = {}) => ({
|
|
99
|
+
...offer(days),
|
|
100
|
+
pass: {
|
|
101
|
+
price,
|
|
102
|
+
minutes: o.passMinutes,
|
|
103
|
+
days,
|
|
104
|
+
total: money(o.priceCents * days),
|
|
105
|
+
maxDays: o.maxDays,
|
|
106
|
+
header: o.header,
|
|
107
|
+
buy: days > 1 ? `${buyUrl}?days=${days}` : buyUrl,
|
|
108
|
+
buyDays: `${buyUrl}?days=<n>`,
|
|
109
|
+
},
|
|
84
110
|
...extra,
|
|
85
111
|
});
|
|
86
112
|
|
|
@@ -96,12 +122,15 @@ export function createGateway(options = {}) {
|
|
|
96
122
|
const html = (body, status) =>
|
|
97
123
|
new Response(body, { status, headers: { 'content-type': 'text/html; charset=utf-8', ...noStore } });
|
|
98
124
|
|
|
99
|
-
const pageCtx = () => ({
|
|
125
|
+
const pageCtx = (days = 1) => ({
|
|
126
|
+
days,
|
|
127
|
+
total: money(o.priceCents * days),
|
|
100
128
|
siteName: o.siteName,
|
|
101
129
|
siteUrl: o.siteUrl,
|
|
102
130
|
buyUrl,
|
|
103
131
|
price,
|
|
104
132
|
minutes: o.passMinutes,
|
|
133
|
+
maxDays: o.maxDays,
|
|
105
134
|
header: o.header,
|
|
106
135
|
enabled,
|
|
107
136
|
offer: offer(),
|
|
@@ -129,14 +158,35 @@ export function createGateway(options = {}) {
|
|
|
129
158
|
async function sell(request) {
|
|
130
159
|
const ua = request.headers.get('user-agent') ?? '';
|
|
131
160
|
const proofHeader = request.headers.get('x-payment');
|
|
161
|
+
const asked = daysFrom(request);
|
|
132
162
|
|
|
133
163
|
if (proofHeader) {
|
|
134
|
-
if (!enabled) return json(receipt({ error: 'Payments are not switched on here.' }), 402);
|
|
164
|
+
if (!enabled) return json(receipt(asked, { error: 'Payments are not switched on here.' }), 402);
|
|
135
165
|
const payment = decodePayment(proofHeader);
|
|
136
|
-
if (!payment) return json(receipt({ error: 'X-PAYMENT is not base64 JSON.' }), 402);
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
166
|
+
if (!payment) return json(receipt(asked, { error: 'X-PAYMENT is not base64 JSON.' }), 402);
|
|
167
|
+
const unit = expectedFor(payment, offer(1));
|
|
168
|
+
if (!unit) return json(receipt(asked, { error: 'Proof does not match an offered network.' }), 402);
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
* The money decides the term, not the URL. A proof is an authorization
|
|
172
|
+
* for an exact value, and the value the buyer signed is what CoinPay
|
|
173
|
+
* will move -- so the days it buys are read off the proof: a whole
|
|
174
|
+
* number of day-prices, at most maxDays. `?days=` shaped the offer the
|
|
175
|
+
* buyer read; if they then signed for a different multiple, they get
|
|
176
|
+
* what they paid for, and if they signed for something that is not a
|
|
177
|
+
* multiple they get nothing, before anyone is charged.
|
|
178
|
+
*/
|
|
179
|
+
const days = daysPaid(paidValueOf(payment), unit.amount, o.maxDays);
|
|
180
|
+
if (!days) {
|
|
181
|
+
return json(
|
|
182
|
+
receipt(asked, {
|
|
183
|
+
error: `Pay a whole number of days: ${unit.amount} per day in the token's smallest unit, up to ${o.maxDays} days. Add ?days=<n> to ${buyUrl} for the offer.`,
|
|
184
|
+
}),
|
|
185
|
+
402,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const expected = expectedFor(payment, offer(days));
|
|
189
|
+
const term = days * o.passMinutes * 60;
|
|
140
190
|
|
|
141
191
|
const now = Math.floor(Date.now() / 1000);
|
|
142
192
|
const coinpay = { apiKey: o.coinpay.apiKey, baseUrl: o.coinpay.baseUrl, fetch: o.fetch };
|
|
@@ -145,7 +195,7 @@ export function createGateway(options = {}) {
|
|
|
145
195
|
let expiresAt = null;
|
|
146
196
|
let replayed = false;
|
|
147
197
|
if (result.ok) {
|
|
148
|
-
expiresAt = now +
|
|
198
|
+
expiresAt = now + term;
|
|
149
199
|
} else if (result.replay) {
|
|
150
200
|
/*
|
|
151
201
|
* Paid once, lost the answer, asked again with the same proof. Answered
|
|
@@ -158,12 +208,12 @@ export function createGateway(options = {}) {
|
|
|
158
208
|
const paid = await settleAgain(payment, coinpay);
|
|
159
209
|
const validBefore = validBeforeOf(payment);
|
|
160
210
|
if (paid && validBefore) {
|
|
161
|
-
expiresAt = Math.min(now +
|
|
211
|
+
expiresAt = Math.min(now + term, validBefore + term);
|
|
162
212
|
replayed = true;
|
|
163
213
|
}
|
|
164
214
|
}
|
|
165
215
|
if (!expiresAt || expiresAt <= now) {
|
|
166
|
-
return json(receipt({ error: result.reason ?? 'Payment could not be settled.' }), 402);
|
|
216
|
+
return json(receipt(days, { error: result.reason ?? 'Payment could not be settled.' }), 402);
|
|
167
217
|
}
|
|
168
218
|
|
|
169
219
|
const ref = nonceOf(payment) ?? result.ref ?? null;
|
|
@@ -178,6 +228,8 @@ export function createGateway(options = {}) {
|
|
|
178
228
|
expiresAt: expires,
|
|
179
229
|
userAgent: ua,
|
|
180
230
|
priceCents: o.priceCents,
|
|
231
|
+
days,
|
|
232
|
+
totalCents: o.priceCents * days,
|
|
181
233
|
currency: o.currency,
|
|
182
234
|
});
|
|
183
235
|
} catch {
|
|
@@ -189,6 +241,8 @@ export function createGateway(options = {}) {
|
|
|
189
241
|
ok: true,
|
|
190
242
|
pass: pass.token,
|
|
191
243
|
expires_at: expires,
|
|
244
|
+
days,
|
|
245
|
+
minutes: days * o.passMinutes,
|
|
192
246
|
header: o.header,
|
|
193
247
|
replayed,
|
|
194
248
|
use: `curl -H "${o.header}: ${pass.token}" ${o.siteUrl}/`,
|
|
@@ -198,8 +252,8 @@ export function createGateway(options = {}) {
|
|
|
198
252
|
);
|
|
199
253
|
}
|
|
200
254
|
|
|
201
|
-
if (wantsHtml(request.headers.get('accept'))) return html(o.page(pageCtx()), 402);
|
|
202
|
-
return json(receipt({ error: `Payment required for training crawlers. Read ${buyUrl} for how.` }), 402);
|
|
255
|
+
if (wantsHtml(request.headers.get('accept'))) return html(o.page(pageCtx(asked)), 402);
|
|
256
|
+
return json(receipt(asked, { error: `Payment required for training crawlers. Read ${buyUrl} for how.` }), 402);
|
|
203
257
|
}
|
|
204
258
|
|
|
205
259
|
/**
|
|
@@ -253,6 +307,42 @@ export function createGateway(options = {}) {
|
|
|
253
307
|
/** Whether the caller would rather read a page than a JSON offer. */
|
|
254
308
|
export const wantsHtml = (accept = '') => String(accept ?? '').toLowerCase().includes('text/html');
|
|
255
309
|
|
|
310
|
+
/** The value a proof authorizes, in the token's smallest unit, or null. */
|
|
311
|
+
export function paidValueOf(payment) {
|
|
312
|
+
const raw = payment?.payload?.authorization?.value;
|
|
313
|
+
if (raw === undefined || raw === null || raw === '') return null;
|
|
314
|
+
try {
|
|
315
|
+
const value = BigInt(raw);
|
|
316
|
+
return value > 0n ? value : null;
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* How many terms a paid value buys at `unit` per term: a whole number in
|
|
324
|
+
* [1, maxDays], or 0 when it is not one. Integer arithmetic on the smallest
|
|
325
|
+
* unit, so a price that is not a round number of cents still divides exactly.
|
|
326
|
+
*
|
|
327
|
+
* @param {bigint|null} value
|
|
328
|
+
* @param {string|number|bigint} unit
|
|
329
|
+
* @param {number} maxDays
|
|
330
|
+
* @returns {number}
|
|
331
|
+
*/
|
|
332
|
+
export function daysPaid(value, unit, maxDays) {
|
|
333
|
+
if (value === null) return 0;
|
|
334
|
+
let per;
|
|
335
|
+
try {
|
|
336
|
+
per = BigInt(unit);
|
|
337
|
+
} catch {
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
if (per <= 0n || value % per !== 0n) return 0;
|
|
341
|
+
const days = value / per;
|
|
342
|
+
if (days < 1n || days > BigInt(maxDays)) return 0;
|
|
343
|
+
return Number(days);
|
|
344
|
+
}
|
|
345
|
+
|
|
256
346
|
function normalise(options) {
|
|
257
347
|
const siteUrl = String(options.siteUrl ?? '').replace(/\/+$/, '');
|
|
258
348
|
if (!siteUrl) throw new Error('createGateway needs siteUrl');
|
|
@@ -268,6 +358,7 @@ function normalise(options) {
|
|
|
268
358
|
priceCents: Number.isFinite(options.priceCents) ? options.priceCents : 100,
|
|
269
359
|
currency: options.currency ?? 'USD',
|
|
270
360
|
passMinutes: Number.isFinite(options.passMinutes) && options.passMinutes > 0 ? options.passMinutes : 1440,
|
|
361
|
+
maxDays: Number.isInteger(options.maxDays) && options.maxDays >= 1 ? options.maxDays : 30,
|
|
271
362
|
header: String(options.header ?? 'x-crawl-pass').toLowerCase(),
|
|
272
363
|
path: options.path ?? '/crawl',
|
|
273
364
|
openPaths: options.openPaths ?? [],
|
package/src/page.js
CHANGED
|
@@ -55,6 +55,9 @@ export function renderPage(ctx) {
|
|
|
55
55
|
training = [],
|
|
56
56
|
retrieval = [],
|
|
57
57
|
contact,
|
|
58
|
+
days = 1,
|
|
59
|
+
total = price,
|
|
60
|
+
maxDays = 30,
|
|
58
61
|
} = ctx;
|
|
59
62
|
const window =
|
|
60
63
|
minutes === 1440
|
|
@@ -82,7 +85,12 @@ export function renderPage(ctx) {
|
|
|
82
85
|
<h1>Training crawlers pay for access here.</h1>
|
|
83
86
|
<p class="mut">People read <a href="${esc(siteUrl)}">${esc(siteName)}</a> free. So do search engines and the retrieval crawlers behind AI answers, because they send readers back. A crawler that copies pages into a training corpus sends nobody back, so it pays for the time it spends.</p>
|
|
84
87
|
|
|
85
|
-
<div class="price">${esc(price)} <span class="mut" style="font-size:1rem;font-weight:400">for ${esc(window)} of requests</span></div>
|
|
88
|
+
<div class="price">${esc(days > 1 ? total : price)} <span class="mut" style="font-size:1rem;font-weight:400">for ${esc(days > 1 ? `${days} × ${window}` : window)} of requests</span></div>
|
|
89
|
+
${
|
|
90
|
+
days > 1
|
|
91
|
+
? `<p class="mut">This offer is for ${days} days at ${esc(price)} a day. The plain page at <code>${esc(buyUrl)}</code> quotes one.</p>`
|
|
92
|
+
: `<p class="mut">Want longer? Add <code>?days=<n></code> to this URL for an offer of up to ${maxDays} days at ${esc(price)} a day, or simply pay a whole multiple of the price: the pass lasts as many days as you paid for.</p>`
|
|
93
|
+
}
|
|
86
94
|
${
|
|
87
95
|
enabled
|
|
88
96
|
? ''
|
|
@@ -93,13 +101,15 @@ ${
|
|
|
93
101
|
<ol>
|
|
94
102
|
<li>Any page you fetch answers <code>402 Payment Required</code>. This page, fetched with <code>Accept: application/json</code>, returns the x402 offer: USDC, <code>exact</code> scheme, on ${esc(networks || 'Base, Polygon or Ethereum')}.</li>
|
|
95
103
|
<li>Sign the payment and retry with the proof in an <code>X-PAYMENT</code> header. The response is a JSON receipt carrying a pass.</li>
|
|
96
|
-
<li>Send the pass in <code>${esc(header)}</code> on every request
|
|
104
|
+
<li>Send the pass in <code>${esc(header)}</code> on every request until it expires — ${esc(window)} per day paid, so a proof for three times the price buys three. When it expires, buy another. The sale is the pass, not the page: fetch the page again with the pass.</li>
|
|
97
105
|
</ol>
|
|
98
106
|
|
|
99
107
|
<h2>Pay with the CoinPay CLI</h2>
|
|
100
108
|
<p>Settlement is by CoinPay: the buyer's USDC goes straight to the site's wallet and CoinPay's relayer pays the gas, so you need USDC and nothing else.</p>
|
|
101
109
|
<pre><code>npm install -g @profullstack/coinpay
|
|
102
|
-
coinpay x402 pay ${esc(buyUrl)} --output pass.json
|
|
110
|
+
coinpay x402 pay ${esc(buyUrl)} --output pass.json
|
|
111
|
+
# or a week at once:
|
|
112
|
+
coinpay x402 pay "${esc(buyUrl)}?days=7" --output pass.json</code></pre>
|
|
103
113
|
<p>The command fetches this page, reads the offer, opens a browser tab to approve the payment with the CoinPay Wallet extension or any EIP-6963 wallet (MetaMask, Rabby, Coinbase Wallet), and writes the receipt to <code>pass.json</code>. Then:</p>
|
|
104
114
|
<pre><code>PASS=$(node -p "require('./pass.json').pass")
|
|
105
115
|
curl -H "${esc(header)}: $PASS" ${esc(siteUrl)}/</code></pre>
|
|
@@ -109,7 +119,8 @@ curl -H "${esc(header)}: $PASS" ${esc(siteUrl)}/</code></pre>
|
|
|
109
119
|
# 402 with { "x402Version": 2, "accepts": [ ... ] }
|
|
110
120
|
# sign an EIP-3009 transferWithAuthorization for one entry, then:
|
|
111
121
|
curl -sS -H "X-PAYMENT: <base64 proof>" ${esc(buyUrl)}
|
|
112
|
-
# 200 with { "ok": true, "pass": "cp_...", "expires_at": "...", "header": "${esc(header)}" }</code></pre>
|
|
122
|
+
# 200 with { "ok": true, "pass": "cp_...", "expires_at": "...", "days": 1, "header": "${esc(header)}" }</code></pre>
|
|
123
|
+
<p class="mut">The days a proof buys are read off the value it authorizes: a whole multiple of the one-day amount, up to ${maxDays}. <code>?days=<n></code> only changes what the offer quotes, so a standard client that pays exactly what is asked gets <em>n</em> days.</p>
|
|
113
124
|
<p class="mut">The proof is x402 v2 in CoinPay's dialect: <code>{ x402Version: 2, scheme: "exact", network: "<CAIP-2>", payload: { signature, authorization } }</code>, base64-encoded. A proof is single-use; retrying with the same one returns the same pass, not a second charge.</p>
|
|
114
125
|
|
|
115
126
|
<h2>Who pays and who does not</h2>
|