@pr402/client 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +190 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +127 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @pr402/client
|
|
2
|
+
|
|
3
|
+
Lightweight [x402 v2](https://github.com/coinbase/x402/blob/main/specs/x402-specification-v2.md) client for buyer agents on Solana. Ships as one package with two faces:
|
|
4
|
+
|
|
5
|
+
- **Library** (`X402AgentClient`) — drop into any Node project that needs to call paid HTTP endpoints.
|
|
6
|
+
- **CLI** (`pr402-buy`) — single binary for quick tests, scripted pipelines, and one-off agents.
|
|
7
|
+
|
|
8
|
+
Both share one code path, so behavior is identical.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Library and CLI both install from the same package:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Project dependency
|
|
16
|
+
npm install @pr402/client
|
|
17
|
+
|
|
18
|
+
# Global CLI
|
|
19
|
+
npm install -g @pr402/client
|
|
20
|
+
|
|
21
|
+
# Or run without installing
|
|
22
|
+
npx @pr402/client pr402-buy --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Use the CLI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pr402-buy \
|
|
29
|
+
--resource https://some.seller.com/api/thing \
|
|
30
|
+
--payer ~/.config/solana/buyer.json \
|
|
31
|
+
--mint 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
On success the paid body is printed to stdout. Pipeline-ready:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pr402-buy -r … -p … -m … | jq .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Flags
|
|
41
|
+
|
|
42
|
+
| Flag | Purpose |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `--resource, -r <URL>` | **Required.** Paid URL. `pr402-buy` GETs first; 200 returns directly, 402 kicks off the payment loop. |
|
|
45
|
+
| `--payer, -p <PATH>` | **Required.** Solana keypair JSON (array of 64 bytes, same shape as `solana-keygen new`). |
|
|
46
|
+
| `--mint, -m <PUBKEY>` | **Required.** Base58 mint to pay with — must match one of `accepts[].asset` from the 402. |
|
|
47
|
+
| `--auto-wrap-sol` | Advanced: inject WSOL wrap instructions. Off by default. |
|
|
48
|
+
| `--verbose, -v` | Print bodies at each step on stderr. |
|
|
49
|
+
| `--help, -h` | Usage info. |
|
|
50
|
+
|
|
51
|
+
### Exit codes
|
|
52
|
+
|
|
53
|
+
| Code | Meaning |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `0` | Resource fetched successfully. |
|
|
56
|
+
| `1` | Usage / flag error. |
|
|
57
|
+
| `2` | Network or HTTP transport failure. |
|
|
58
|
+
| `3` | Protocol-level rejection (facilitator or seller returned a definitive error). |
|
|
59
|
+
|
|
60
|
+
## Use the library
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { X402AgentClient, X402Error } from "@miraland-labs/pr402-client";
|
|
64
|
+
import { Keypair } from "@solana/web3.js";
|
|
65
|
+
import * as fs from "node:fs";
|
|
66
|
+
|
|
67
|
+
const bytes = JSON.parse(fs.readFileSync("/path/to/keypair.json", "utf8"));
|
|
68
|
+
const wallet = Keypair.fromSecretKey(new Uint8Array(bytes));
|
|
69
|
+
const client = new X402AgentClient(wallet);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const res = await client.fetchWithAutoPay(
|
|
73
|
+
"https://some.seller.com/api/thing",
|
|
74
|
+
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", // devnet USDC
|
|
75
|
+
);
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
console.log(data);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (e instanceof X402Error && e.code === "MINT_NOT_ACCEPTED") {
|
|
80
|
+
console.error("Available mints:", e.availableMints);
|
|
81
|
+
} else {
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The method returns a standard `Response` so you can inspect headers (`PAYMENT-RESPONSE`, correlation ids, caches) before reading the body.
|
|
88
|
+
|
|
89
|
+
## What this does under the hood
|
|
90
|
+
|
|
91
|
+
1. `GET <resource>` — expect 402.
|
|
92
|
+
2. `POST <facilitator>/build-exact-payment-tx` — receive an unsigned `VersionedTransaction` plus a `verifyBodyTemplate` with everything except the signed tx pre-filled.
|
|
93
|
+
3. Sign the transaction locally at `payerSignatureIndex`.
|
|
94
|
+
4. `GET <resource>` with `PAYMENT-SIGNATURE` header — the seller forwards the proof to the facilitator, which verifies and settles in one hop, and returns the paid response.
|
|
95
|
+
|
|
96
|
+
The client never builds a Solana transaction from scratch — the facilitator does. CU limits, token-program branches, PDA derivations, and fee-payer details come from the facilitator's build response, so buyer code stays forward-compatible across facilitator policy changes.
|
|
97
|
+
|
|
98
|
+
## Error codes
|
|
99
|
+
|
|
100
|
+
All protocol-level errors come as `X402Error` with a typed `code` field:
|
|
101
|
+
|
|
102
|
+
| Code | Meaning |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `MINT_NOT_ACCEPTED` | Preferred mint isn't in the seller's `accepts[]`. `e.availableMints` lists the options. |
|
|
105
|
+
| `BLOCKHASH_EXPIRED` | Build response is too old; request a fresh build. `e.expiresAt` is the Unix timestamp. |
|
|
106
|
+
| `RATE_LIMITED` | Facilitator returned 429. `e.retryAfterSecs` indicates the wait. |
|
|
107
|
+
| `BUILD_FAILED` | Facilitator rejected the build request. `e.httpStatus` + the message carry the reason. |
|
|
108
|
+
| `MISSING_CAPABILITIES_URL` | Seller didn't integrate with a facilitator; contact them. |
|
|
109
|
+
| `MISSING_ACCEPTS` / `MISSING_VERIFY_TEMPLATE` / `MISSING_TRANSACTION` | Seller or facilitator configuration issue. |
|
|
110
|
+
| `UNEXPECTED_STATUS` | Seller returned something other than 200 or 402. |
|
|
111
|
+
| `TRANSPORT` | Network or serialization error. |
|
|
112
|
+
|
|
113
|
+
## Supported schemes
|
|
114
|
+
|
|
115
|
+
- `v2:solana:exact` (UniversalSettle) — today.
|
|
116
|
+
- `v2:solana:sla-escrow` — not yet.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
Apache-2.0
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pr402-buy — one-shot buyer CLI (TypeScript).
|
|
4
|
+
*
|
|
5
|
+
* Runs the full x402 lifecycle against any seller URL: fetch 402 → build → sign →
|
|
6
|
+
* verify → settle → retry. Seller-agnostic; uses the same `X402AgentClient` the
|
|
7
|
+
* library exposes so the CLI and the importable API evolve together.
|
|
8
|
+
*
|
|
9
|
+
* Distribution:
|
|
10
|
+
* - Installed via the published npm package (`npm i -g @pr402/client`),
|
|
11
|
+
* then: `pr402-buy --resource <url> --payer ~/.config/solana/id.json --mint <mint>`.
|
|
12
|
+
* - Or one-shot without installing: `npx @pr402/client pr402-buy ...`.
|
|
13
|
+
* - No Rust toolchain needed. Works anywhere Node ≥ 18 runs.
|
|
14
|
+
*
|
|
15
|
+
* Flags are intentionally a subset of the Rust `pr402-buy` binary so that scripts can
|
|
16
|
+
* target either implementation interchangeably; the underlying behavior is identical.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* pr402-buy — one-shot buyer CLI (TypeScript).
|
|
5
|
+
*
|
|
6
|
+
* Runs the full x402 lifecycle against any seller URL: fetch 402 → build → sign →
|
|
7
|
+
* verify → settle → retry. Seller-agnostic; uses the same `X402AgentClient` the
|
|
8
|
+
* library exposes so the CLI and the importable API evolve together.
|
|
9
|
+
*
|
|
10
|
+
* Distribution:
|
|
11
|
+
* - Installed via the published npm package (`npm i -g @pr402/client`),
|
|
12
|
+
* then: `pr402-buy --resource <url> --payer ~/.config/solana/id.json --mint <mint>`.
|
|
13
|
+
* - Or one-shot without installing: `npx @pr402/client pr402-buy ...`.
|
|
14
|
+
* - No Rust toolchain needed. Works anywhere Node ≥ 18 runs.
|
|
15
|
+
*
|
|
16
|
+
* Flags are intentionally a subset of the Rust `pr402-buy` binary so that scripts can
|
|
17
|
+
* target either implementation interchangeably; the underlying behavior is identical.
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
const fs = __importStar(require("node:fs"));
|
|
54
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
55
|
+
const index_js_1 = require("./index.js");
|
|
56
|
+
const USAGE = `pr402-buy — one-shot buyer for x402 v2 resources.
|
|
57
|
+
|
|
58
|
+
Usage:
|
|
59
|
+
pr402-buy --resource <URL> --payer <KEYPAIR_PATH> --mint <MINT>
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
--resource, -r <URL> Seller resource URL. GET first; if 200, done. If 402, pay + retry.
|
|
63
|
+
--payer, -p <PATH> Path to a Solana keypair JSON (array of 64 bytes, same as solana-keygen output).
|
|
64
|
+
--mint, -m <PUBKEY> Base58 mint to pay with. Picks the matching accepts[] line from the 402 body.
|
|
65
|
+
--auto-wrap-sol Ask the facilitator to wrap SOL automatically when paying with WSOL.
|
|
66
|
+
--verbose, -v Print bodies at each step.
|
|
67
|
+
--help, -h This help.
|
|
68
|
+
|
|
69
|
+
Exit codes:
|
|
70
|
+
0 resource fetched successfully
|
|
71
|
+
1 usage / flag error
|
|
72
|
+
2 network or HTTP transport failure
|
|
73
|
+
3 protocol-level failure (facilitator / seller rejected the flow)
|
|
74
|
+
`;
|
|
75
|
+
function parseArgs(argv) {
|
|
76
|
+
const out = {
|
|
77
|
+
resource: "",
|
|
78
|
+
payer: "",
|
|
79
|
+
mint: "",
|
|
80
|
+
verbose: false,
|
|
81
|
+
autoWrapSol: false,
|
|
82
|
+
help: false,
|
|
83
|
+
};
|
|
84
|
+
for (let i = 0; i < argv.length; i++) {
|
|
85
|
+
const a = argv[i];
|
|
86
|
+
const next = () => argv[++i] ?? "";
|
|
87
|
+
switch (a) {
|
|
88
|
+
case "--resource":
|
|
89
|
+
case "-r":
|
|
90
|
+
out.resource = next();
|
|
91
|
+
break;
|
|
92
|
+
case "--payer":
|
|
93
|
+
case "-p":
|
|
94
|
+
out.payer = next();
|
|
95
|
+
break;
|
|
96
|
+
case "--mint":
|
|
97
|
+
case "-m":
|
|
98
|
+
out.mint = next();
|
|
99
|
+
break;
|
|
100
|
+
case "--auto-wrap-sol":
|
|
101
|
+
out.autoWrapSol = true;
|
|
102
|
+
break;
|
|
103
|
+
case "--verbose":
|
|
104
|
+
case "-v":
|
|
105
|
+
out.verbose = true;
|
|
106
|
+
break;
|
|
107
|
+
case "--help":
|
|
108
|
+
case "-h":
|
|
109
|
+
out.help = true;
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
// Unknown flag: bail early so typos don't silently succeed with defaults.
|
|
113
|
+
throw new Error(`unknown flag: ${a}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
function loadKeypair(path) {
|
|
119
|
+
// Solana CLI keypair format: JSON array of 64 bytes.
|
|
120
|
+
const raw = fs.readFileSync(path, "utf8");
|
|
121
|
+
const bytes = JSON.parse(raw);
|
|
122
|
+
if (!Array.isArray(bytes) || bytes.length !== 64) {
|
|
123
|
+
throw new Error(`keypair file ${path} must be a JSON array of 64 bytes, got ${Array.isArray(bytes) ? `array of ${bytes.length}` : typeof bytes}`);
|
|
124
|
+
}
|
|
125
|
+
return web3_js_1.Keypair.fromSecretKey(new Uint8Array(bytes));
|
|
126
|
+
}
|
|
127
|
+
async function main() {
|
|
128
|
+
let args;
|
|
129
|
+
try {
|
|
130
|
+
args = parseArgs(process.argv.slice(2));
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
process.stderr.write(`${e.message}\n\n${USAGE}`);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
if (args.help) {
|
|
137
|
+
process.stdout.write(USAGE);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
if (!args.resource || !args.payer || !args.mint) {
|
|
141
|
+
process.stderr.write(`missing required flag.\n\n${USAGE}`);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
const payer = loadKeypair(args.payer);
|
|
145
|
+
if (args.verbose) {
|
|
146
|
+
process.stderr.write(`payer: ${payer.publicKey.toBase58()}\n`);
|
|
147
|
+
}
|
|
148
|
+
const client = new index_js_1.X402AgentClient(payer);
|
|
149
|
+
try {
|
|
150
|
+
const res = await client.fetchWithAutoPay(args.resource, args.mint, {
|
|
151
|
+
autoWrapSol: args.autoWrapSol,
|
|
152
|
+
});
|
|
153
|
+
const text = await res.text();
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
process.stderr.write(`resource retry failed (HTTP ${res.status}): ${text}\n`);
|
|
156
|
+
return 3;
|
|
157
|
+
}
|
|
158
|
+
const paymentResponse = res.headers.get("PAYMENT-RESPONSE");
|
|
159
|
+
if (args.verbose && paymentResponse) {
|
|
160
|
+
process.stderr.write(`PAYMENT-RESPONSE (base64): ${paymentResponse}\n`);
|
|
161
|
+
}
|
|
162
|
+
process.stdout.write(text);
|
|
163
|
+
// Newline only when stdout is a TTY so piped output stays byte-identical.
|
|
164
|
+
if (process.stdout.isTTY)
|
|
165
|
+
process.stdout.write("\n");
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
if (e instanceof index_js_1.X402Error) {
|
|
170
|
+
// Protocol-level error codes come with actionable context — surface them.
|
|
171
|
+
process.stderr.write(`${e.code}: ${e.message}\n`);
|
|
172
|
+
if (e.availableMints?.length) {
|
|
173
|
+
process.stderr.write(`available mints: ${e.availableMints.join(", ")}\n`);
|
|
174
|
+
}
|
|
175
|
+
if (e.retryAfterSecs) {
|
|
176
|
+
process.stderr.write(`retry after: ${e.retryAfterSecs}s\n`);
|
|
177
|
+
}
|
|
178
|
+
if (e.expiresAt) {
|
|
179
|
+
process.stderr.write(`blockhash expired at unix ${e.expiresAt}\n`);
|
|
180
|
+
}
|
|
181
|
+
return 3;
|
|
182
|
+
}
|
|
183
|
+
process.stderr.write(`transport error: ${e.message}\n`);
|
|
184
|
+
return 2;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
main().then((code) => process.exit(code), (e) => {
|
|
188
|
+
process.stderr.write(`unexpected: ${e.stack ?? e}\n`);
|
|
189
|
+
process.exit(2);
|
|
190
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Keypair } from '@solana/web3.js';
|
|
2
|
+
/**
|
|
3
|
+
* Specific, actionable error codes for autonomous agent remediation.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* try { await client.fetchWithAutoPay(url, mint); }
|
|
8
|
+
* catch (e) {
|
|
9
|
+
* if (e instanceof X402Error && e.code === 'MINT_NOT_ACCEPTED')
|
|
10
|
+
* console.log('Available mints:', e.availableMints);
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export type X402ErrorCode = 'UNEXPECTED_STATUS' | 'MISSING_ACCEPTS' | 'MINT_NOT_ACCEPTED' | 'MISSING_CAPABILITIES_URL' | 'BUILD_FAILED' | 'MISSING_VERIFY_TEMPLATE' | 'MISSING_TRANSACTION' | 'BLOCKHASH_EXPIRED' | 'RATE_LIMITED' | 'TRANSPORT';
|
|
15
|
+
export declare class X402Error extends Error {
|
|
16
|
+
readonly code: X402ErrorCode;
|
|
17
|
+
/** Mints accepted by the resource (only for MINT_NOT_ACCEPTED). */
|
|
18
|
+
readonly availableMints?: string[];
|
|
19
|
+
/** HTTP status from the facilitator (only for BUILD_FAILED / UNEXPECTED_STATUS). */
|
|
20
|
+
readonly httpStatus?: number;
|
|
21
|
+
/** Seconds to wait before retrying (only for RATE_LIMITED). */
|
|
22
|
+
readonly retryAfterSecs?: number;
|
|
23
|
+
/** UNIX epoch when blockhash expires (only for BLOCKHASH_EXPIRED). */
|
|
24
|
+
readonly expiresAt?: number;
|
|
25
|
+
constructor(code: X402ErrorCode, message: string, extra?: {
|
|
26
|
+
availableMints?: string[];
|
|
27
|
+
httpStatus?: number;
|
|
28
|
+
retryAfterSecs?: number;
|
|
29
|
+
expiresAt?: number;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export interface FetchAutoPayOptions extends RequestInit {
|
|
33
|
+
/** If true, the facilitator SDK build step will inject wSOL wrapping instructions automatically. */
|
|
34
|
+
autoWrapSol?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Lightweight pr402 agent client.
|
|
38
|
+
*
|
|
39
|
+
* Wraps standard `fetch()` to automatically detect `402 Payment Required`,
|
|
40
|
+
* delegate transaction construction to the pr402 Facilitator,
|
|
41
|
+
* sign locally with Ed25519, and retry the original request with proof.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const client = new X402AgentClient(myKeypair);
|
|
46
|
+
* const res = await client.fetchWithAutoPay(url, usdcMint);
|
|
47
|
+
* const data = await res.json();
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare class X402AgentClient {
|
|
51
|
+
private wallet;
|
|
52
|
+
constructor(wallet: Keypair);
|
|
53
|
+
/**
|
|
54
|
+
* GET a 402-gated resource. If challenged, automatically build, sign, and settle.
|
|
55
|
+
*
|
|
56
|
+
* On success (seller returns 200) the retry request carries the signed
|
|
57
|
+
* `verifyBodyTemplate` as a **`PAYMENT-SIGNATURE`** header (x402 v2). The value
|
|
58
|
+
* is base64(UTF-8 JSON). Sellers in this ecosystem accept either base64 or raw
|
|
59
|
+
* JSON in that header; this client emits base64 for URL-safety.
|
|
60
|
+
*
|
|
61
|
+
* @param url - The target API endpoint
|
|
62
|
+
* @param preferredMint - Base58 mint address of the token you want to pay with
|
|
63
|
+
* @param options - Optional extra fetch options (headers, autoWrapSol, etc.)
|
|
64
|
+
* @throws {X402Error} with a specific `code` for each failure mode
|
|
65
|
+
*/
|
|
66
|
+
fetchWithAutoPay(url: string, preferredMint: string, options?: FetchAutoPayOptions): Promise<Response>;
|
|
67
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.X402AgentClient = exports.X402Error = void 0;
|
|
4
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
5
|
+
class X402Error extends Error {
|
|
6
|
+
constructor(code, message, extra) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'X402Error';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.availableMints = extra?.availableMints;
|
|
11
|
+
this.httpStatus = extra?.httpStatus;
|
|
12
|
+
this.retryAfterSecs = extra?.retryAfterSecs;
|
|
13
|
+
this.expiresAt = extra?.expiresAt;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.X402Error = X402Error;
|
|
17
|
+
/**
|
|
18
|
+
* Lightweight pr402 agent client.
|
|
19
|
+
*
|
|
20
|
+
* Wraps standard `fetch()` to automatically detect `402 Payment Required`,
|
|
21
|
+
* delegate transaction construction to the pr402 Facilitator,
|
|
22
|
+
* sign locally with Ed25519, and retry the original request with proof.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const client = new X402AgentClient(myKeypair);
|
|
27
|
+
* const res = await client.fetchWithAutoPay(url, usdcMint);
|
|
28
|
+
* const data = await res.json();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
class X402AgentClient {
|
|
32
|
+
constructor(wallet) {
|
|
33
|
+
this.wallet = wallet;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* GET a 402-gated resource. If challenged, automatically build, sign, and settle.
|
|
37
|
+
*
|
|
38
|
+
* On success (seller returns 200) the retry request carries the signed
|
|
39
|
+
* `verifyBodyTemplate` as a **`PAYMENT-SIGNATURE`** header (x402 v2). The value
|
|
40
|
+
* is base64(UTF-8 JSON). Sellers in this ecosystem accept either base64 or raw
|
|
41
|
+
* JSON in that header; this client emits base64 for URL-safety.
|
|
42
|
+
*
|
|
43
|
+
* @param url - The target API endpoint
|
|
44
|
+
* @param preferredMint - Base58 mint address of the token you want to pay with
|
|
45
|
+
* @param options - Optional extra fetch options (headers, autoWrapSol, etc.)
|
|
46
|
+
* @throws {X402Error} with a specific `code` for each failure mode
|
|
47
|
+
*/
|
|
48
|
+
async fetchWithAutoPay(url, preferredMint, options) {
|
|
49
|
+
const res = await fetch(url, options);
|
|
50
|
+
if (res.status === 200)
|
|
51
|
+
return res;
|
|
52
|
+
if (res.status !== 402)
|
|
53
|
+
throw new X402Error('UNEXPECTED_STATUS', `Unexpected HTTP status ${res.status}. Expected 200 (free) or 402 (payment required).`, { httpStatus: res.status });
|
|
54
|
+
// ── Step 1: Parse the 402 Challenge ─────────────────────────────
|
|
55
|
+
const requirement = await res.json();
|
|
56
|
+
const accepts = requirement.accepts || [];
|
|
57
|
+
if (accepts.length === 0)
|
|
58
|
+
throw new X402Error('MISSING_ACCEPTS', "The 402 response has no 'accepts' array. The Resource Provider's payment configuration is invalid. Contact the RP operator.");
|
|
59
|
+
const availableMints = accepts
|
|
60
|
+
.map((a) => a.asset)
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
const rule = accepts.find((a) => a.asset === preferredMint);
|
|
63
|
+
if (!rule)
|
|
64
|
+
throw new X402Error('MINT_NOT_ACCEPTED', `Resource does not accept mint ${preferredMint}. Available mints: [${availableMints.join(', ')}]. Pick one from this list.`, { availableMints });
|
|
65
|
+
const capUrl = rule.extra?.capabilitiesUrl;
|
|
66
|
+
if (!capUrl)
|
|
67
|
+
throw new X402Error('MISSING_CAPABILITIES_URL', 'This 402-gated resource did not provide extra.capabilitiesUrl. The Resource Provider has not completed Facilitator integration. See docs/SELLER_INTEGRATION.md.');
|
|
68
|
+
// ── Step 2: Ask Facilitator to build the tx ─────────────────────
|
|
69
|
+
const facilitatorBase = capUrl.replace('/capabilities', '');
|
|
70
|
+
const buildRes = await fetch(`${facilitatorBase}/build-exact-payment-tx`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
payer: this.wallet.publicKey.toBase58(),
|
|
75
|
+
accepted: rule,
|
|
76
|
+
resource: requirement.resource,
|
|
77
|
+
skipSourceBalanceCheck: true,
|
|
78
|
+
autoWrapSol: options?.autoWrapSol,
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
if (buildRes.status === 429) {
|
|
82
|
+
const retryAfter = parseInt(buildRes.headers.get('retry-after') || '60', 10);
|
|
83
|
+
throw new X402Error('RATE_LIMITED', `Facilitator rate-limited this request. Retry after ${retryAfter}s.`, { retryAfterSecs: retryAfter });
|
|
84
|
+
}
|
|
85
|
+
if (!buildRes.ok) {
|
|
86
|
+
const detail = await buildRes.text();
|
|
87
|
+
throw new X402Error('BUILD_FAILED', `Facilitator build-exact-payment-tx returned HTTP ${buildRes.status}: ${detail}`, { httpStatus: buildRes.status });
|
|
88
|
+
}
|
|
89
|
+
const buildJson = await buildRes.json();
|
|
90
|
+
// BUY-3: Check blockhash expiry before signing
|
|
91
|
+
if (buildJson.recentBlockhashExpiresAt) {
|
|
92
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
93
|
+
if (nowSec >= buildJson.recentBlockhashExpiresAt) {
|
|
94
|
+
throw new X402Error('BLOCKHASH_EXPIRED', `The embedded blockhash expired at UNIX ${buildJson.recentBlockhashExpiresAt}. Request a fresh build from the Facilitator.`, { expiresAt: buildJson.recentBlockhashExpiresAt });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!buildJson.verifyBodyTemplate)
|
|
98
|
+
throw new X402Error('MISSING_VERIFY_TEMPLATE', "Facilitator response is missing 'verifyBodyTemplate'. The Facilitator may be running an incompatible version.");
|
|
99
|
+
if (!buildJson.transaction)
|
|
100
|
+
throw new X402Error('MISSING_TRANSACTION', "Facilitator response is missing 'transaction'. The Facilitator may be running an incompatible version.");
|
|
101
|
+
// ── Step 3: Sign the unsigned transaction ───────────────────────
|
|
102
|
+
const txBytes = Uint8Array.from(atob(buildJson.transaction), (c) => c.charCodeAt(0));
|
|
103
|
+
const vtx = web3_js_1.VersionedTransaction.deserialize(txBytes);
|
|
104
|
+
vtx.sign([this.wallet]);
|
|
105
|
+
const signedB64 = btoa(String.fromCharCode(...vtx.serialize()));
|
|
106
|
+
// ── Step 4: Inject signature into verify body template ──────────
|
|
107
|
+
const verifyBody = buildJson.verifyBodyTemplate;
|
|
108
|
+
verifyBody.paymentPayload.payload.transaction = signedB64;
|
|
109
|
+
const proofB64 = btoa(JSON.stringify(verifyBody));
|
|
110
|
+
// ── Step 5: Replay original request with proof ──────────────────
|
|
111
|
+
//
|
|
112
|
+
// x402 v2 uses the `PAYMENT-SIGNATURE` header name (see the x402 HTTP
|
|
113
|
+
// transport-v2 spec and `public/agent-integration.md` in this repo).
|
|
114
|
+
// v1 used `X-PAYMENT`; every seller in this ecosystem today — aethervane,
|
|
115
|
+
// spl-token-balance-serverless, x402-seller-starter — reads only
|
|
116
|
+
// `PAYMENT-SIGNATURE`, so emitting `X-PAYMENT` silently fails with a
|
|
117
|
+
// repeated 402. Emit the canonical v2 header exclusively.
|
|
118
|
+
return fetch(url, {
|
|
119
|
+
...options,
|
|
120
|
+
headers: {
|
|
121
|
+
...(options?.headers || {}),
|
|
122
|
+
'PAYMENT-SIGNATURE': proofB64,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.X402AgentClient = X402AgentClient;
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pr402/client",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Lightweight NPM library + CLI for autonomous agents paying HTTP 402 APIs settled on Solana via pr402.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"pr402-buy": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"postbuild": "node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\""
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@solana/web3.js": "^1.87.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"x402",
|
|
31
|
+
"solana",
|
|
32
|
+
"pr402",
|
|
33
|
+
"facilitator",
|
|
34
|
+
"agent"
|
|
35
|
+
],
|
|
36
|
+
"license": "Apache-2.0",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/miralandlabs/pr402"
|
|
40
|
+
}
|
|
41
|
+
}
|