@profullstack/x402-client 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/x402.js +189 -0
- package/index.d.ts +197 -0
- package/package.json +41 -0
- package/src/client.js +230 -0
- package/src/eip712.js +256 -0
- package/src/index.js +34 -0
- package/src/passes.js +117 -0
- package/src/types.js +18 -0
- package/src/wallet.js +59 -0
- package/src/x402.js +278 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Profullstack, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @profullstack/x402-client
|
|
2
|
+
|
|
3
|
+
Pay an [x402](https://x402.org) `402 Payment Required` from Node with a local key. Sign the EIP-3009 authorization, retry with the proof, keep the pass.
|
|
4
|
+
|
|
5
|
+
A crawler, an agent, or a script that meets a paid site needs three things: to read the offer, to sign it with a key it holds, and to remember what it bought so the next thousand requests are free. That is the whole package: a `fetch` that does all three, a CLI for shells, and the header that lets Lightpanda, Puppeteer or curl through a site sold by [@profullstack/x402-gateway](https://github.com/profullstack/x402-gateway) or any other x402 v2 server.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @profullstack/x402-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Two dependencies, `@noble/curves` and `@noble/hashes`, for one hash function and one signature. No ethers, no viem, no RPC: the payer never sends a transaction and never needs ETH. The facilitator broadcasts the authorization and pays the gas; the wallet holds USDC and signs.
|
|
12
|
+
|
|
13
|
+
## Fetch
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { createClient } from '@profullstack/x402-client';
|
|
17
|
+
|
|
18
|
+
const client = createClient({ key: process.env.X402_PRIVATE_KEY });
|
|
19
|
+
|
|
20
|
+
const res = await client.fetch('https://rssamplifier.com/some/page');
|
|
21
|
+
// 402 → paid → the page. The pass is filed by origin, so:
|
|
22
|
+
const again = await client.fetch('https://rssamplifier.com/another/page');
|
|
23
|
+
// presents the pass, pays nothing.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`fetch` behaves like the global one until it sees a 402 carrying an x402 offer. Then it picks the first option it can sign, signs an EIP-3009 `TransferWithAuthorization` for exactly the offered amount to exactly the offered address, and asks again with the proof in `X-PAYMENT` and `PAYMENT-SIGNATURE`. A gateway answers with a receipt naming a pass, which is filed and presented on every later request to that origin; a plain x402 resource answers with the resource, which is returned as it came.
|
|
27
|
+
|
|
28
|
+
It pays once per request and never guesses: a second 402 after a proof means the payment was refused, and that is thrown as an `X402Error` with `code: 'rejected'` and the server's reason, not retried with a fresh signature.
|
|
29
|
+
|
|
30
|
+
## Pay, explicitly
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const result = await client.pay('https://rssamplifier.com/crawl');
|
|
34
|
+
result.payer; // 0x… the address that signed
|
|
35
|
+
result.amountUsd; // 1
|
|
36
|
+
result.network; // 'eip155:8453'
|
|
37
|
+
result.receipt.pass; // 'cp_…' the day pass
|
|
38
|
+
result.receipt.header; // 'x-crawl-pass' where to put it
|
|
39
|
+
result.receipt.expires; // ISO timestamp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`pay` asks the URL for its offer (as JSON), signs, and files whatever pass comes back. Give it `{ offer }` to skip the first request when you already hold one.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
createClient({
|
|
48
|
+
key, // 32-byte secp256k1 private key, hex. Or `wallet`.
|
|
49
|
+
maxUsd: 5, // refuse any single payment above this. Default 5.
|
|
50
|
+
networks: ['eip155:8453'], // only pay on these chains, e.g. the one you funded
|
|
51
|
+
store: 'file', // file passes in ~/.config/x402-client/passes.json; default memory
|
|
52
|
+
userAgent: 'MyBot/1.0 (+https://…)',
|
|
53
|
+
validForSeconds, // authorization lifetime; defaults to the offer's maxTimeoutSeconds
|
|
54
|
+
fetch, // your own fetch, for proxies and tests
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The ceiling is the one setting to think about. A client that holds a key and pays whatever it is asked is a client a hostile server can drain by asking. A day of crawling on a gateway costs a dollar; five is room for a dearer site and not for a mistake.
|
|
59
|
+
|
|
60
|
+
`networks` matters because the server's `accepts` is in its order of preference, and the first entry may be a chain your wallet has no USDC on. A signed authorization on an empty wallet verifies and then fails to settle, and the server answers 402 as if you had never paid.
|
|
61
|
+
|
|
62
|
+
## The CLI
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
npm install -g @profullstack/x402-client
|
|
66
|
+
export X402_PRIVATE_KEY=0x…
|
|
67
|
+
|
|
68
|
+
x402 address # fund this
|
|
69
|
+
x402 pay https://site/crawl # receipt as JSON
|
|
70
|
+
x402 pass https://site/crawl # the pass, buying one if none is on file
|
|
71
|
+
x402 fetch https://site/page # the page, paying if it asks
|
|
72
|
+
x402 passes # what is on file
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The key is read from the environment or `--key-file`, never from an argument. Passes are filed in `$XDG_CONFIG_HOME/x402-client/passes.json` with mode 0600, so one invocation buys and the rest present:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
curl -H "x-crawl-pass: $(x402 pass https://site/crawl)" https://site/page
|
|
79
|
+
lightpanda fetch --http-header "x-crawl-pass: $(x402 pass https://site/crawl)" https://site/page
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Lightpanda and Puppeteer
|
|
83
|
+
|
|
84
|
+
Lightpanda has no plugin system, and does not need one: a gateway sells time, not pages, so the browser only has to carry the header a payment bought. The shell line above is the whole integration. For a script, [`examples/lightpanda.mjs`](examples/lightpanda.mjs) connects Puppeteer to `lightpanda serve`, presents the filed pass, and on a 402 pays from Node and reloads. The browser never holds the key. Nothing in it is Lightpanda-specific; it works against Chrome.
|
|
85
|
+
|
|
86
|
+
## What is signed
|
|
87
|
+
|
|
88
|
+
x402's `exact` scheme on EVM is EIP-3009: the payer signs a `TransferWithAuthorization` under the **token's** EIP-712 domain, and whoever holds the signature can move exactly that amount to exactly that address, once, before `validBefore`. The signature is the payment. The client:
|
|
89
|
+
|
|
90
|
+
- takes the domain from the offer's `extra: { name, version }`, falling back to a table of USDC deployments read from the contracts, and refuses to sign a token whose domain it does not know, because a guessed domain recovers to the wrong address and fails with no diagnostic
|
|
91
|
+
- sets `validAfter` to 0 and bounds `validBefore` by the offer's own `maxTimeoutSeconds`, so no spendable authorization outlives the server's promise to settle
|
|
92
|
+
- uses a fresh random 32-byte nonce per payment
|
|
93
|
+
- emits `v` as 27/28, which is what `ecrecover` in `transferWithAuthorization` expects
|
|
94
|
+
|
|
95
|
+
The signer is a port of the one in CoinPay Wallet, which was checked byte for byte against ethers; the test suite holds this port to a digest and signature produced by that original.
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
`X402Error` carries a `code`:
|
|
100
|
+
|
|
101
|
+
| code | meaning |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `no-key` | the client has no key and was asked to pay |
|
|
104
|
+
| `no-offer` | the URL answered 402 with no x402 in it, or did not answer 402 |
|
|
105
|
+
| `no-option` | nothing in `accepts` is `exact` on an EVM chain in `networks` |
|
|
106
|
+
| `too-expensive` | over `maxUsd`, or a token this client cannot price |
|
|
107
|
+
| `rejected` | the server refused the proof; `body` has its reason |
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/bin/x402.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* x402 from a shell.
|
|
4
|
+
*
|
|
5
|
+
* x402 pay <url> pay the offer at a URL, print the receipt as JSON
|
|
6
|
+
* x402 pass <url> print the pass for a URL's site, buying one if needed
|
|
7
|
+
* x402 fetch <url> fetch a URL, paying if it asks, print the body
|
|
8
|
+
* x402 address the address the key controls, to fund it
|
|
9
|
+
* x402 passes the passes on file
|
|
10
|
+
*
|
|
11
|
+
* The key comes from `X402_PRIVATE_KEY` or `--key-file <path>`, never from an
|
|
12
|
+
* argument: a key on the command line is in every process list and shell
|
|
13
|
+
* history on the box. Passes are filed in `$XDG_CONFIG_HOME/x402-client/
|
|
14
|
+
* passes.json` so a second invocation, or curl, or Lightpanda, reuses what
|
|
15
|
+
* the first one bought:
|
|
16
|
+
*
|
|
17
|
+
* lightpanda fetch --http-header "x-crawl-pass: $(x402 pass https://site/crawl)" https://site/page
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
import { createClient, X402Error } from '../src/client.js';
|
|
23
|
+
import { fileStore, originOf } from '../src/passes.js';
|
|
24
|
+
|
|
25
|
+
const HELP = `usage: x402 <command> [options]
|
|
26
|
+
|
|
27
|
+
commands
|
|
28
|
+
pay <url> pay the offer at <url> and print the receipt (JSON)
|
|
29
|
+
pass <url> print the pass for <url>'s site, buying one if none is on file
|
|
30
|
+
fetch <url> fetch <url>, paying if it asks, and print the body
|
|
31
|
+
address print the address the key controls
|
|
32
|
+
passes list the passes on file
|
|
33
|
+
|
|
34
|
+
options
|
|
35
|
+
--key-file <path> read the private key from a file (or set X402_PRIVATE_KEY)
|
|
36
|
+
--max-usd <n> refuse any single payment above this (default 5)
|
|
37
|
+
--network <caip2> only pay on this network, e.g. eip155:8453; repeatable
|
|
38
|
+
--store <path> where passes are filed (default ~/.config/x402-client/passes.json)
|
|
39
|
+
-H, --header <k: v> extra request header; repeatable
|
|
40
|
+
--json print machine-readable output for pass and fetch
|
|
41
|
+
-h, --help this
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
function parse(argv) {
|
|
45
|
+
const opts = { headers: [], networks: [], json: false };
|
|
46
|
+
const rest = [];
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const a = argv[i];
|
|
49
|
+
const next = () => {
|
|
50
|
+
if (i + 1 >= argv.length) fail(`${a} needs a value`);
|
|
51
|
+
return argv[++i];
|
|
52
|
+
};
|
|
53
|
+
if (a === '-h' || a === '--help') opts.help = true;
|
|
54
|
+
else if (a === '--json') opts.json = true;
|
|
55
|
+
else if (a === '--key-file') opts.keyFile = next();
|
|
56
|
+
else if (a === '--max-usd') opts.maxUsd = Number(next());
|
|
57
|
+
else if (a === '--network') opts.networks.push(next());
|
|
58
|
+
else if (a === '--store') opts.store = next();
|
|
59
|
+
else if (a === '-H' || a === '--header') opts.headers.push(next());
|
|
60
|
+
else if (a.startsWith('-')) fail(`unknown option ${a}`);
|
|
61
|
+
else rest.push(a);
|
|
62
|
+
}
|
|
63
|
+
return { opts, command: rest[0], args: rest.slice(1) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fail(message, code = 2) {
|
|
67
|
+
process.stderr.write(`x402: ${message}\n`);
|
|
68
|
+
process.exit(code);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function keyFrom(opts) {
|
|
72
|
+
if (opts.keyFile) return readFileSync(opts.keyFile, 'utf8').trim();
|
|
73
|
+
return process.env.X402_PRIVATE_KEY?.trim() || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function headersFrom(list) {
|
|
77
|
+
const headers = {};
|
|
78
|
+
for (const line of list) {
|
|
79
|
+
const at = line.indexOf(':');
|
|
80
|
+
if (at < 1) fail(`header must be "Name: value", got ${JSON.stringify(line)}`);
|
|
81
|
+
headers[line.slice(0, at).trim()] = line.slice(at + 1).trim();
|
|
82
|
+
}
|
|
83
|
+
return headers;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
const { opts, command, args } = parse(process.argv.slice(2));
|
|
88
|
+
if (opts.help || !command) {
|
|
89
|
+
process.stdout.write(HELP);
|
|
90
|
+
process.exit(opts.help ? 0 : 2);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const store = fileStore(opts.store);
|
|
94
|
+
const key = keyFrom(opts);
|
|
95
|
+
const needsKey = command === 'pay' || command === 'address';
|
|
96
|
+
if (needsKey && !key) fail('no key: set X402_PRIVATE_KEY or pass --key-file <path>');
|
|
97
|
+
|
|
98
|
+
const client = createClient({
|
|
99
|
+
key: key ?? undefined,
|
|
100
|
+
store,
|
|
101
|
+
maxUsd: Number.isFinite(opts.maxUsd) ? opts.maxUsd : undefined,
|
|
102
|
+
networks: opts.networks.length ? opts.networks : undefined,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const url = args[0];
|
|
106
|
+
const needsUrl = command === 'pay' || command === 'pass' || command === 'fetch';
|
|
107
|
+
if (needsUrl && !url) fail(`${command} needs a URL`);
|
|
108
|
+
|
|
109
|
+
if (command === 'address') {
|
|
110
|
+
process.stdout.write(`${client.address}\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (command === 'passes') {
|
|
115
|
+
const all = store.all();
|
|
116
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(all, null, 2)}\n`);
|
|
117
|
+
else {
|
|
118
|
+
const rows = Object.entries(all);
|
|
119
|
+
if (rows.length === 0) process.stdout.write(`no passes on file (${store.path})\n`);
|
|
120
|
+
for (const [origin, pass] of rows) {
|
|
121
|
+
process.stdout.write(`${origin}\t${pass.header}\t${pass.expires ?? 'no expiry'}\t${pass.token.slice(0, 24)}…\n`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (command === 'pay') {
|
|
128
|
+
const result = await client.pay(url, { init: { headers: headersFrom(opts.headers) } });
|
|
129
|
+
process.stdout.write(`${JSON.stringify(receiptOf(result), null, 2)}\n`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (command === 'pass') {
|
|
134
|
+
let pass = client.passFor(url);
|
|
135
|
+
if (!pass) {
|
|
136
|
+
if (!key) fail(`no pass on file for ${originOf(url)} and no key to buy one: set X402_PRIVATE_KEY`);
|
|
137
|
+
const result = await client.pay(url, { init: { headers: headersFrom(opts.headers) } });
|
|
138
|
+
if (!result.receipt.pass) fail(`${url} took the payment but issued no pass`, 1);
|
|
139
|
+
pass = client.passFor(url);
|
|
140
|
+
}
|
|
141
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(pass, null, 2)}\n`);
|
|
142
|
+
else process.stdout.write(`${pass.token}\n`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (command === 'fetch') {
|
|
147
|
+
const res = await client.fetch(url, { headers: headersFrom(opts.headers) });
|
|
148
|
+
const text = await res.text();
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
process.stdout.write(
|
|
151
|
+
`${JSON.stringify({ status: res.status, headers: Object.fromEntries(res.headers), body: text }, null, 2)}\n`,
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
process.stdout.write(text);
|
|
155
|
+
if (!text.endsWith('\n')) process.stdout.write('\n');
|
|
156
|
+
}
|
|
157
|
+
if (res.status >= 400) process.exit(1);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fail(`unknown command ${command}\n\n${HELP}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function receiptOf(result) {
|
|
165
|
+
return {
|
|
166
|
+
status: result.status,
|
|
167
|
+
payer: result.payer,
|
|
168
|
+
payTo: result.payTo,
|
|
169
|
+
network: result.network,
|
|
170
|
+
amount: result.amount,
|
|
171
|
+
amountUsd: result.amountUsd,
|
|
172
|
+
nonce: result.nonce,
|
|
173
|
+
pass: result.receipt.pass,
|
|
174
|
+
header: result.receipt.header,
|
|
175
|
+
expires: result.receipt.expires,
|
|
176
|
+
settlement: result.receipt.settlement,
|
|
177
|
+
body: result.body,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch((err) => {
|
|
182
|
+
if (err instanceof X402Error) {
|
|
183
|
+
process.stderr.write(`x402: ${err.message}\n`);
|
|
184
|
+
if (err.body && typeof err.body === 'object') process.stderr.write(`${JSON.stringify(err.body, null, 2)}\n`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
process.stderr.write(`x402: ${err?.stack ?? err}\n`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
export interface TypedDataField {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
}
|
|
5
|
+
export type TypedDataTypes = Record<string, TypedDataField[]>;
|
|
6
|
+
export interface TypedDataDomain {
|
|
7
|
+
name?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
chainId?: number | bigint | string;
|
|
10
|
+
verifyingContract?: string;
|
|
11
|
+
salt?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Wallet {
|
|
15
|
+
/** EIP-55 checksummed address. */
|
|
16
|
+
readonly address: string;
|
|
17
|
+
signTypedData(domain: TypedDataDomain, types: TypedDataTypes, primaryType: string, message: Record<string, unknown>): string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One option in a 402's `accepts`. */
|
|
21
|
+
export interface AcceptEntry {
|
|
22
|
+
scheme?: string;
|
|
23
|
+
network: string;
|
|
24
|
+
amount?: string;
|
|
25
|
+
maxAmountRequired?: string;
|
|
26
|
+
asset?: string;
|
|
27
|
+
payTo?: string;
|
|
28
|
+
resource?: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
mimeType?: string;
|
|
31
|
+
maxTimeoutSeconds?: number;
|
|
32
|
+
extra?: { name?: string; version?: string; [k: string]: unknown };
|
|
33
|
+
[k: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Offer {
|
|
37
|
+
x402Version?: number;
|
|
38
|
+
accepts: AcceptEntry[];
|
|
39
|
+
[k: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Authorization {
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
value: string;
|
|
46
|
+
validAfter: string;
|
|
47
|
+
validBefore: string;
|
|
48
|
+
nonce: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Payment {
|
|
52
|
+
x402Version: number;
|
|
53
|
+
scheme: string;
|
|
54
|
+
network: string;
|
|
55
|
+
payload: { signature: string; authorization: Authorization };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StoredPass {
|
|
59
|
+
token: string;
|
|
60
|
+
header: string;
|
|
61
|
+
expires: string | null;
|
|
62
|
+
boughtAt: string;
|
|
63
|
+
url: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PassStore {
|
|
67
|
+
get(origin: string): StoredPass | null;
|
|
68
|
+
set(origin: string, pass: StoredPass): void;
|
|
69
|
+
delete(origin: string): void;
|
|
70
|
+
all(): Record<string, StoredPass>;
|
|
71
|
+
path?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface Receipt {
|
|
75
|
+
pass: string | null;
|
|
76
|
+
header: string;
|
|
77
|
+
expires: string | null;
|
|
78
|
+
settlement: object | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface PayResult {
|
|
82
|
+
status: number;
|
|
83
|
+
body: unknown;
|
|
84
|
+
receipt: Receipt;
|
|
85
|
+
payer: string;
|
|
86
|
+
network: string;
|
|
87
|
+
amount: string;
|
|
88
|
+
amountUsd: number | null;
|
|
89
|
+
payTo: string | undefined;
|
|
90
|
+
nonce: string;
|
|
91
|
+
payment: Payment;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ClientOptions {
|
|
95
|
+
/** 32-byte secp256k1 private key, hex. */
|
|
96
|
+
key?: string | Uint8Array;
|
|
97
|
+
/** A wallet, instead of a key. */
|
|
98
|
+
wallet?: Wallet;
|
|
99
|
+
/** Only pay on these CAIP-2 networks, in the server's order of preference. */
|
|
100
|
+
networks?: string[];
|
|
101
|
+
/** Refuse any single payment above this many dollars. Default 5. */
|
|
102
|
+
maxUsd?: number;
|
|
103
|
+
/** How long a signed authorization stays valid; defaults to the offer's maxTimeoutSeconds, else 600. */
|
|
104
|
+
validForSeconds?: number;
|
|
105
|
+
/** A User-Agent to send when the caller sets none. */
|
|
106
|
+
userAgent?: string;
|
|
107
|
+
/** Where passes are filed: a store, or 'file' for the default file. Default memory. */
|
|
108
|
+
store?: PassStore | 'file';
|
|
109
|
+
/** A fetch to use instead of the global one. */
|
|
110
|
+
fetch?: typeof fetch;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface X402Client {
|
|
114
|
+
/** The paying address, or null for a client without a key. */
|
|
115
|
+
readonly address: string | null;
|
|
116
|
+
readonly store: PassStore;
|
|
117
|
+
/** fetch that presents a filed pass and pays a 402 once. */
|
|
118
|
+
fetch(input: string | URL | Request, init?: RequestInit, options?: { pay?: boolean }): Promise<Response>;
|
|
119
|
+
/** Pay for a URL (asking it for the offer first unless one is given) and file any pass. */
|
|
120
|
+
pay(url: string | URL | Request, options?: { offer?: Offer; init?: RequestInit }): Promise<PayResult>;
|
|
121
|
+
/** The live pass filed for a URL's origin. */
|
|
122
|
+
passFor(url: string | URL): StoredPass | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type X402ErrorCode = 'no-key' | 'no-offer' | 'no-option' | 'too-expensive' | 'rejected';
|
|
126
|
+
|
|
127
|
+
export class X402Error extends Error {
|
|
128
|
+
code: X402ErrorCode | string;
|
|
129
|
+
status: number | null;
|
|
130
|
+
body: unknown;
|
|
131
|
+
offer: Offer | null;
|
|
132
|
+
url: string | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const DEFAULT_MAX_USD: number;
|
|
136
|
+
export const X402_VERSION: number;
|
|
137
|
+
export const KNOWN_TOKEN_DOMAINS: Record<string, { name: string; version: string; decimals: number }>;
|
|
138
|
+
export const TRANSFER_WITH_AUTHORIZATION_TYPES: TypedDataTypes;
|
|
139
|
+
|
|
140
|
+
export function createClient(options?: ClientOptions): X402Client;
|
|
141
|
+
export function walletFromKey(privateKey: string | Uint8Array): Wallet;
|
|
142
|
+
export function randomWallet(): { wallet: Wallet; privateKey: string };
|
|
143
|
+
|
|
144
|
+
export function memoryStore(): PassStore;
|
|
145
|
+
export function fileStore(path?: string): PassStore;
|
|
146
|
+
export function defaultPassPath(): string;
|
|
147
|
+
export function originOf(url: string | URL): string;
|
|
148
|
+
export function isLive(pass: StoredPass | null | undefined, now?: number): boolean;
|
|
149
|
+
|
|
150
|
+
export function evmChainId(network: string): number | null;
|
|
151
|
+
export function readOffer(response: Response): Promise<{ offer: Offer | null; body: unknown }>;
|
|
152
|
+
export function isOffer(value: unknown): value is Offer;
|
|
153
|
+
export function selectAccept(accepts: AcceptEntry[], options?: { networks?: string[] }): AcceptEntry | null;
|
|
154
|
+
export function domainFor(entry: AcceptEntry): { name: string; version: string; chainId: number; verifyingContract: string };
|
|
155
|
+
export function requiredAmount(entry: AcceptEntry): string;
|
|
156
|
+
export function amountUsd(entry: AcceptEntry): number | null;
|
|
157
|
+
export function randomNonce(): string;
|
|
158
|
+
export function buildAuthorization(args: {
|
|
159
|
+
from: string;
|
|
160
|
+
to: string;
|
|
161
|
+
value: string | number | bigint;
|
|
162
|
+
validForSeconds?: number;
|
|
163
|
+
nonce?: string;
|
|
164
|
+
now?: number;
|
|
165
|
+
}): Authorization;
|
|
166
|
+
export function signPayment(
|
|
167
|
+
entry: AcceptEntry,
|
|
168
|
+
wallet: Wallet,
|
|
169
|
+
options?: { validForSeconds?: number; nonce?: string; now?: number },
|
|
170
|
+
): { payment: Payment; header: string; authorization: Authorization; domain: TypedDataDomain };
|
|
171
|
+
export function encodePaymentHeader(payment: Payment): string;
|
|
172
|
+
export function decodePaymentHeader(header: string | null | undefined): Payment | null;
|
|
173
|
+
export function readReceipt(response: Response, body: unknown): Receipt;
|
|
174
|
+
|
|
175
|
+
export function eip712Digest(domain: TypedDataDomain, types: TypedDataTypes, primaryType: string, message: Record<string, unknown>): Uint8Array;
|
|
176
|
+
export function signTypedData(
|
|
177
|
+
domain: TypedDataDomain,
|
|
178
|
+
types: TypedDataTypes,
|
|
179
|
+
primaryType: string,
|
|
180
|
+
message: Record<string, unknown>,
|
|
181
|
+
privateKey: Uint8Array,
|
|
182
|
+
): string;
|
|
183
|
+
export function recoverTypedDataSigner(
|
|
184
|
+
domain: TypedDataDomain,
|
|
185
|
+
types: TypedDataTypes,
|
|
186
|
+
primaryType: string,
|
|
187
|
+
message: Record<string, unknown>,
|
|
188
|
+
signature: string,
|
|
189
|
+
): string;
|
|
190
|
+
export function addressOfPublicKey(publicKey: Uint8Array): string;
|
|
191
|
+
export function checksumAddress(address: string): string;
|
|
192
|
+
export function hashStruct(primaryType: string, types: TypedDataTypes, data: Record<string, unknown>): Uint8Array;
|
|
193
|
+
export function hashDomain(domain: TypedDataDomain): Uint8Array;
|
|
194
|
+
export function encodeType(primaryType: string, types: TypedDataTypes): string;
|
|
195
|
+
export function typeHash(primaryType: string, types: TypedDataTypes): Uint8Array;
|
|
196
|
+
export function hexToBytes(hex: string): Uint8Array;
|
|
197
|
+
export function bytesToHex(bytes: Uint8Array): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@profullstack/x402-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pay an x402 402 from Node with a local key: sign the EIP-3009 authorization, retry with the proof, keep the pass. A fetch, a CLI, and the header that lets Lightpanda, Puppeteer or curl through a paid site.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"x402",
|
|
8
|
+
"402",
|
|
9
|
+
"payment-required",
|
|
10
|
+
"eip-3009",
|
|
11
|
+
"eip-712",
|
|
12
|
+
"usdc",
|
|
13
|
+
"coinpay",
|
|
14
|
+
"crawler",
|
|
15
|
+
"lightpanda",
|
|
16
|
+
"puppeteer",
|
|
17
|
+
"headless",
|
|
18
|
+
"agent",
|
|
19
|
+
"micropayment"
|
|
20
|
+
],
|
|
21
|
+
"repository": { "type": "git", "url": "git+https://github.com/profullstack/x402-client.git" },
|
|
22
|
+
"homepage": "https://github.com/profullstack/x402-client#readme",
|
|
23
|
+
"bugs": { "url": "https://github.com/profullstack/x402-client/issues" },
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Profullstack, LLC",
|
|
26
|
+
"bin": { "x402": "./bin/x402.js" },
|
|
27
|
+
"exports": {
|
|
28
|
+
".": { "types": "./index.d.ts", "import": "./src/index.js" }
|
|
29
|
+
},
|
|
30
|
+
"types": "./index.d.ts",
|
|
31
|
+
"files": ["src", "bin", "index.d.ts", "README.md", "LICENSE"],
|
|
32
|
+
"engines": { "node": ">=20.19" },
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test test/*.test.js"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@noble/curves": "^2.4.0",
|
|
39
|
+
"@noble/hashes": "^2.4.0"
|
|
40
|
+
}
|
|
41
|
+
}
|