@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 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
+ });
@@ -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
+ }