agentspend 0.1.10 → 0.2.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 CHANGED
@@ -1,43 +1,38 @@
1
- # AgentSpend
1
+ # agentspend
2
2
 
3
- CLI for managing AI agent payment methods — cards (Stripe) and crypto wallets (USDC on Base).
3
+ AgentSpend CLI for calling x402 endpoints through AgentSpend Cloud.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install -g agentspend
8
+ npm install
9
+ npm run build
9
10
  ```
10
11
 
11
- ## Card setup (Stripe)
12
+ ## Commands
12
13
 
13
14
  ```bash
14
- # Start card setup — opens Stripe in your browser
15
- agentspend card setup
16
-
17
- # Check setup status
18
- agentspend card status
15
+ agentspend configure
16
+ agentspend pay <url> [--method GET|POST|PUT|PATCH|DELETE|...] [--body '{"hello":"world"}'] [--header 'Content-Type:application/json'] [--max-cost 5.000000]
17
+ agentspend check <url> [--method GET|POST|PUT|PATCH|DELETE|...] [--body '{"hello":"world"}'] [--header 'Content-Type:application/json']
18
+ agentspend status
19
19
  ```
20
20
 
21
- `card setup` creates a card via the AgentSpend API, opens the Stripe setup URL in your browser, and polls until the card is ready. The `card_id` is saved to `~/.agentspend/card.json`.
21
+ ## Credentials
22
22
 
23
- Agents use the `card_id` to pay services by sending `x-card-id: card_xxx` in request headers.
23
+ Credentials are stored at `~/.agentspend/credentials.json`.
24
24
 
25
- ## Wallet (crypto)
25
+ ## Local backend dev CLI
26
26
 
27
- ```bash
28
- # Generate a new wallet for x402 payments
29
- agentspend wallet create
27
+ Use the local entrypoint (hardcoded to `http://127.0.0.1:8787`) when testing against a local backend:
30
28
 
31
- # Show address, network, and USDC balance
32
- agentspend wallet status
29
+ ```bash
30
+ bun run dev:local -- configure
33
31
  ```
34
32
 
35
- `wallet create` generates a local private key and saves it to `~/.agentspend/wallet.json`. Fund the address with USDC on Base to pay x402-enabled services.
36
-
37
- ## Configuration
33
+ Build a non-published local binary:
38
34
 
39
- | Variable | Description |
40
- |----------|-------------|
41
- | `AGENTSPEND_API_URL` | Platform API base URL (default: `https://api.agentspend.co`) |
42
-
43
- Config files are stored in `~/.agentspend/`.
35
+ ```bash
36
+ bun run build
37
+ node dist/dev-index.js configure
38
+ ```
package/SKILL.md ADDED
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: agentspend
3
+ description: Use this skill when you need to: search the web, generate images or pictures, create videos, scrape or read a webpage, take a screenshot of a website, get stock prices or financial data, look up business info, find news articles, translate text, parse PDFs, do OCR on images, convert files, get weather data, or access any external API that requires payment. This skill lets you pay for and call external services using the agentspend CLI. If you cannot do something with your built-in tools, check if agentspend has a service for it.
4
+ ---
5
+
6
+ # When To Use This Skill
7
+ Use this skill whenever the user asks to:
8
+ - find data from external APIs
9
+ - call an endpoint
10
+ - fetch/search information outside local context
11
+ - pay for x402 services
12
+ - generate an image, video, voice, transcription, or music with an external API
13
+ - scrape/extract data from a URL
14
+ - find an API for a task ("is there an API for X?")
15
+
16
+ If the task needs an external paid API, start with `agentspend search`.
17
+
18
+ ## Playbook (Default Workflow)
19
+
20
+ 1. `npx agentspend search "<task>"`
21
+ 2. Confirm cost and constraints with the user (`--max-cost`, budget, allowlist)
22
+ 3. `npx agentspend pay <endpoint> --method ... --header ... --body ... --max-cost ...`
23
+
24
+ ## Setup
25
+
26
+ ```bash
27
+ npx agentspend configure
28
+ ```
29
+
30
+ Opens a URL to add a credit card and set a weekly spending limit. Saves credentials to `~/.agentspend/credentials.json`.
31
+
32
+ If already configured, re-running opens the dashboard to update settings.
33
+
34
+ ## Commands
35
+
36
+ ### Pay
37
+
38
+ ```bash
39
+ npx agentspend pay <url>
40
+ ```
41
+
42
+ Make a paid request. AgentSpend handles the payment automatically. Works with endpoints that support x402 (HTTP 402-based payment protocol for APIs).
43
+
44
+ **Options:**
45
+ - `--method <method>` — HTTP method (default: `GET`)
46
+ - `--body <body>` — Request body (JSON or text)
47
+ - `--header <header>` — Header in `key:value` format (repeatable)
48
+ - `--max-cost <usd>` — Maximum acceptable charge in USD (up to 6 decimal places)
49
+
50
+ **Returns:**
51
+ - Response body from the endpoint
52
+ - Charge amount and remaining weekly budget
53
+
54
+ **Example:**
55
+
56
+ ```bash
57
+ npx agentspend pay <url> \
58
+ --method POST \
59
+ --header "key:value" \
60
+ --body '{"key": "value"}' \
61
+ --max-cost 0.05
62
+ ```
63
+
64
+ ### Check
65
+
66
+ ```bash
67
+ npx agentspend check <url>
68
+ ```
69
+
70
+ Discover an endpoint's price without paying.
71
+
72
+ Important:
73
+ - `check` must use the same request shape you plan to `pay` with.
74
+ - Always pass `--method` for non-GET endpoints.
75
+ - If the endpoint needs headers/body, include the same `--header` and `--body` on `check`.
76
+ - If request shape is wrong, endpoint may return `404`/`400` instead of `402`, and no price can be extracted.
77
+
78
+ **Example:**
79
+
80
+ ```bash
81
+ npx agentspend check <url> \
82
+ --method POST \
83
+ --header "content-type:application/json" \
84
+ --body '{"key":"value"}'
85
+ ```
86
+
87
+ **Returns:**
88
+ - Price in USD
89
+ - Description (if available)
90
+
91
+ ### Search
92
+
93
+ ```bash
94
+ npx agentspend search <keywords>
95
+ ```
96
+
97
+ Keyword search over service names and descriptions in the catalog. Returns up to 5 matching services.
98
+
99
+ **Example:**
100
+
101
+ ```bash
102
+ npx agentspend search "video generation"
103
+ ```
104
+
105
+ ### Status
106
+
107
+ ```bash
108
+ npx agentspend status
109
+ ```
110
+
111
+ Show account spending overview.
112
+
113
+ **Returns:**
114
+ - Weekly budget
115
+ - Amount spent this week
116
+ - Remaining budget
117
+ - Recent charges with amounts, domains, and timestamps
118
+
119
+ ### Configure
120
+
121
+ ```bash
122
+ npx agentspend configure
123
+ ```
124
+
125
+ Run onboarding or open the dashboard to update settings (weekly budget, domain allowlist, payment method).
126
+
127
+ ## Spending Controls
128
+
129
+ - **Weekly budget** — Set during configure. Requests that would exceed the budget are rejected.
130
+ - **Per-request max cost** — Use `--max-cost` on `pay` to reject requests above a price threshold.
131
+ - **Domain allowlist** — Configurable via the dashboard. Requests to non-allowlisted domains are rejected.
132
+
133
+ ## Common Errors
134
+
135
+ - **`WEEKLY_BUDGET_EXCEEDED`** — Weekly spending limit reached. Run `npx agentspend configure` to increase the budget.
136
+ - **`DOMAIN_NOT_ALLOWLISTED`** — The target domain is not in the allowlist. Run `npx agentspend configure` to update allowed domains.
137
+ - **`PRICE_EXCEEDS_MAX`** — Endpoint price is higher than `--max-cost`. Increase the value or remove the flag.
138
+ - **`UNSUPPORTED_PAYMENT_REQUIRED_FORMAT`** — The endpoint returned a 402 response but doesn't use the x402 format AgentSpend supports.
package/dist/cli.js ADDED
@@ -0,0 +1,63 @@
1
+ import { Command } from "commander";
2
+ import { runCheck } from "./commands/check.js";
3
+ import { runConfigure } from "./commands/configure.js";
4
+ import { runPay } from "./commands/pay.js";
5
+ import { runSearch } from "./commands/search.js";
6
+ import { runStatus } from "./commands/status.js";
7
+ import { AgentspendApiClient } from "./lib/api.js";
8
+ function parsePositiveUsd(value) {
9
+ const parsed = Number(value);
10
+ if (!Number.isFinite(parsed) || parsed <= 0) {
11
+ throw new Error(`Expected a positive USD value, received: ${value}`);
12
+ }
13
+ const rounded = Math.round(parsed * 1_000_000) / 1_000_000;
14
+ if (Math.abs(parsed - rounded) > 1e-9) {
15
+ throw new Error(`Expected at most 6 decimal places, received: ${value}`);
16
+ }
17
+ return parsed;
18
+ }
19
+ export async function runCli(options) {
20
+ const program = new Command();
21
+ const apiClient = new AgentspendApiClient(options?.baseUrl);
22
+ const programName = options?.programName ?? "agentspend";
23
+ program.name(programName).description("AgentSpend CLI").version("0.2.0");
24
+ program
25
+ .command("pay")
26
+ .argument("<url>", "URL to call")
27
+ .description("Make a paid request")
28
+ .option("-X, --method <method>", "HTTP method for target request", "GET")
29
+ .option("--body <body>", "Request body (JSON or text)")
30
+ .option("--header <header>", "Header in key:value form", (value, previous) => {
31
+ return [...previous, value];
32
+ }, [])
33
+ .option("--max-cost <usd>", "Maximum acceptable charge in USD (up to 6 decimals)", parsePositiveUsd)
34
+ .action(async (url, commandOptions) => {
35
+ await runPay(apiClient, url, commandOptions);
36
+ });
37
+ program
38
+ .command("check")
39
+ .argument("<url>", "URL to check")
40
+ .description("Discover endpoint price without paying")
41
+ .option("-X, --method <method>", "HTTP method for target request", "GET")
42
+ .option("--body <body>", "Request body (JSON or text)")
43
+ .option("--header <header>", "Header in key:value form", (value, previous) => {
44
+ return [...previous, value];
45
+ }, [])
46
+ .action(async (url, commandOptions) => {
47
+ await runCheck(apiClient, url, commandOptions);
48
+ });
49
+ program
50
+ .command("search")
51
+ .argument("<query...>", "Keyword query")
52
+ .description("Search services by name and description")
53
+ .action(async (queryParts) => {
54
+ await runSearch(apiClient, queryParts.join(" "));
55
+ });
56
+ program.command("status").description("Show weekly budget and recent charges").action(async () => {
57
+ await runStatus(apiClient);
58
+ });
59
+ program.command("configure").description("Run onboarding/configuration flow").action(async () => {
60
+ await runConfigure(apiClient);
61
+ });
62
+ await program.parseAsync(process.argv);
63
+ }
@@ -0,0 +1,40 @@
1
+ import { ApiError } from "../lib/api.js";
2
+ import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
3
+ import { formatUsd, usd6ToUsd } from "../lib/output.js";
4
+ import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";
5
+ export async function runCheck(apiClient, url, options) {
6
+ const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
7
+ try {
8
+ const response = await apiClient.check(apiKey, {
9
+ url,
10
+ method: normalizeMethod(options.method),
11
+ headers: parseHeaders(options.header),
12
+ body: parseBody(options.body),
13
+ });
14
+ if (response.free) {
15
+ if ((response.status ?? 200) >= 400) {
16
+ console.log(`No payment required, but endpoint returned status ${response.status}.`);
17
+ return;
18
+ }
19
+ console.log("This endpoint is free.");
20
+ return;
21
+ }
22
+ const policyUsd = response.price_usd ?? (typeof response.price_usd6 === "number" ? usd6ToUsd(response.price_usd6) : null);
23
+ const price = policyUsd !== null ? formatUsd(policyUsd) : "unavailable";
24
+ const description = response.description ?? "unavailable";
25
+ console.log(`Price: ${price}`);
26
+ console.log(`Description: ${description}`);
27
+ return;
28
+ }
29
+ catch (error) {
30
+ if (error instanceof ApiError) {
31
+ const body = error.body;
32
+ if (error.status === 502 && body?.code === "UNSUPPORTED_PAYMENT_REQUIRED_FORMAT") {
33
+ const headers = (body.details?.header_names ?? []).join(", ");
34
+ console.error(`Endpoint returned 402 with an unsupported payment format. Headers seen: ${headers || "none"}.`);
35
+ return;
36
+ }
37
+ }
38
+ throw error;
39
+ }
40
+ }
@@ -0,0 +1,44 @@
1
+ import { ApiError } from "../lib/api.js";
2
+ import { clearPendingConfigureToken, readCredentials, savePendingConfigureToken, } from "../lib/credentials.js";
3
+ import { claimConfigureToken, getPendingConfigureStatus } from "../lib/auth-flow.js";
4
+ async function tryAuthenticatedConfigure(apiClient, apiKey) {
5
+ try {
6
+ return await apiClient.configure(undefined, apiKey);
7
+ }
8
+ catch (error) {
9
+ if (error instanceof ApiError && error.status === 401) {
10
+ return null;
11
+ }
12
+ throw error;
13
+ }
14
+ }
15
+ export async function runConfigure(apiClient) {
16
+ const credentials = await readCredentials();
17
+ if (credentials) {
18
+ const authenticatedResponse = await tryAuthenticatedConfigure(apiClient, credentials.api_key);
19
+ if (authenticatedResponse) {
20
+ console.log(`Open this URL to configure settings:\n${authenticatedResponse.configure_url}`);
21
+ return;
22
+ }
23
+ }
24
+ const pending = await getPendingConfigureStatus(apiClient);
25
+ if (pending) {
26
+ if (pending.status.claim_status === "awaiting_card") {
27
+ console.log(`Open this URL to configure settings:\n${pending.status.configure_url}`);
28
+ return;
29
+ }
30
+ if (pending.status.claim_status === "ready_to_claim") {
31
+ const apiKey = await claimConfigureToken(apiClient, pending.token);
32
+ const claimedResponse = await tryAuthenticatedConfigure(apiClient, apiKey);
33
+ if (claimedResponse) {
34
+ console.log(`Open this URL to configure settings:\n${claimedResponse.configure_url}`);
35
+ return;
36
+ }
37
+ throw new Error("API key was claimed, but configure session could not be created. Run agentspend configure again.");
38
+ }
39
+ await clearPendingConfigureToken();
40
+ }
41
+ const created = await apiClient.configure();
42
+ await savePendingConfigureToken(created.token);
43
+ console.log(`Open this URL to configure settings:\n${created.configure_url}`);
44
+ }
@@ -1,227 +1,110 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerPayCommand = registerPayCommand;
4
- const promises_1 = require("node:fs/promises");
5
- const node_os_1 = require("node:os");
6
- const node_path_1 = require("node:path");
7
- const CONFIG_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".agentspend");
8
- const CARD_FILE = (0, node_path_1.join)(CONFIG_DIR, "card.json");
9
- const WALLET_FILE = (0, node_path_1.join)(CONFIG_DIR, "wallet.json");
10
- const API_BASE = process.env.AGENTSPEND_API_URL ?? "https://api.agentspend.co";
11
- async function readCardConfig() {
12
- try {
13
- const data = JSON.parse(await (0, promises_1.readFile)(CARD_FILE, "utf-8"));
14
- if (typeof data.card_id === "string" && data.card_id && typeof data.card_secret === "string" && data.card_secret) {
15
- return data;
16
- }
17
- }
18
- catch {
19
- // file doesn't exist or is invalid
20
- }
21
- return null;
1
+ import { ApiError } from "../lib/api.js";
2
+ import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
3
+ import { formatJson, formatUsd, formatUsdEstimate, usd6ToUsd } from "../lib/output.js";
4
+ import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null;
22
7
  }
23
- async function readWalletConfig() {
24
- try {
25
- const data = JSON.parse(await (0, promises_1.readFile)(WALLET_FILE, "utf-8"));
26
- if (typeof data.address === "string" && data.address) {
27
- return data;
28
- }
29
- }
30
- catch {
31
- // file doesn't exist or is invalid
32
- }
33
- return null;
8
+ function readNumber(record, key) {
9
+ const value = record[key];
10
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
34
11
  }
35
- function parseHeaders(headerArgs) {
36
- const headers = {};
37
- for (const h of headerArgs) {
38
- const idx = h.indexOf(":");
39
- if (idx === -1) {
40
- throw new Error(`Invalid header format: "${h}". Use key:value`);
41
- }
42
- headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
43
- }
44
- return headers;
12
+ function readString(record, key) {
13
+ const value = record[key];
14
+ return typeof value === "string" ? value : undefined;
45
15
  }
46
- async function payWithCard(url, cardConfig, body, extraHeaders) {
47
- const headers = {
48
- ...extraHeaders,
49
- "x-card-id": cardConfig.card_id,
50
- };
51
- if (body) {
52
- headers["content-type"] = "application/json";
53
- }
54
- // Try with x-card-id
55
- const res = await fetch(url, {
56
- method: "POST",
57
- headers,
58
- body: body ?? undefined,
59
- });
60
- // Success
61
- if (res.ok) {
62
- console.log("Payment successful (card)");
63
- const data = await res.text();
64
- try {
65
- console.log(JSON.stringify(JSON.parse(data), null, 2));
66
- }
67
- catch {
68
- console.log(data);
69
- }
70
- return;
16
+ function parsePayErrorCode(value) {
17
+ if (typeof value !== "string") {
18
+ return undefined;
71
19
  }
72
- // 402 with agentspend.service_id — need to bind
73
- if (res.status === 402) {
74
- const errorBody = await res.json().catch(() => ({}));
75
- const agentspend = errorBody?.agentspend;
76
- const serviceId = agentspend?.service_id;
77
- if (serviceId) {
78
- console.log(`Binding to service ${serviceId}...`);
79
- const bindRes = await fetch(`${API_BASE}/v1/card/${encodeURIComponent(cardConfig.card_id)}/bind`, {
80
- method: "POST",
81
- headers: { "content-type": "application/json" },
82
- body: JSON.stringify({
83
- card_secret: cardConfig.card_secret,
84
- service_id: serviceId,
85
- }),
86
- });
87
- if (!bindRes.ok) {
88
- const bindError = await bindRes.text();
89
- console.error(`Failed to bind (${bindRes.status}): ${bindError}`);
90
- process.exit(1);
91
- }
92
- console.log("Bound. Retrying payment...");
93
- // Retry with x-card-id
94
- const retryRes = await fetch(url, { method: "POST", headers, body: body ?? undefined });
95
- if (!retryRes.ok) {
96
- console.error(`Retry failed (${retryRes.status}): ${await retryRes.text()}`);
97
- process.exit(1);
98
- }
99
- console.log("Payment successful (card)");
100
- const data = await retryRes.text();
101
- try {
102
- console.log(JSON.stringify(JSON.parse(data), null, 2));
103
- }
104
- catch {
105
- console.log(data);
106
- }
107
- return;
108
- }
20
+ if (value === "PRICE_EXCEEDS_MAX" ||
21
+ value === "PRICE_NOT_CONVERTIBLE" ||
22
+ value === "WEEKLY_BUDGET_EXCEEDED" ||
23
+ value === "DOMAIN_NOT_ALLOWLISTED") {
24
+ return value;
109
25
  }
110
- // Other error
111
- console.error(`Request failed (${res.status}): ${await res.text().catch(() => "")}`);
112
- process.exit(1);
26
+ return undefined;
113
27
  }
114
- async function payWithCrypto(url, walletConfig, body, extraHeaders) {
115
- const headers = { ...extraHeaders };
116
- if (body) {
117
- headers["content-type"] = "application/json";
118
- }
119
- // First request — expect 402
120
- const initialRes = await fetch(url, {
121
- method: "POST",
122
- headers,
123
- body: body ?? undefined,
124
- });
125
- if (initialRes.status !== 402) {
126
- if (initialRes.ok) {
127
- console.log("Request succeeded without payment.");
128
- const data = await initialRes.text();
129
- try {
130
- console.log(JSON.stringify(JSON.parse(data), null, 2));
131
- }
132
- catch {
133
- console.log(data);
134
- }
135
- return;
136
- }
137
- const data = await initialRes.text();
138
- console.error(`Expected 402 but got ${initialRes.status}: ${data}`);
139
- process.exit(1);
28
+ function parsePayErrorBody(body) {
29
+ if (!isRecord(body)) {
30
+ return {};
140
31
  }
141
- // Import x402 and viem dynamically
142
- const { x402Client } = await import("@x402/core/client");
143
- const { x402HTTPClient } = await import("@x402/core/http");
144
- const { registerExactEvmScheme } = await import("@x402/evm/exact/client");
145
- const { privateKeyToAccount } = await import("viem/accounts");
146
- const account = privateKeyToAccount(walletConfig.private_key);
147
- const coreClient = new x402Client();
148
- registerExactEvmScheme(coreClient, { signer: account });
149
- const httpClient = new x402HTTPClient(coreClient);
150
- // Decode payment requirements from 402 response
151
- const paymentRequired = httpClient.getPaymentRequiredResponse((name) => initialRes.headers.get(name), await initialRes.clone().json().catch(() => undefined));
152
- // Sign payment
153
- const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
154
- const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
155
- // Resend with payment header
156
- const paidRes = await fetch(url, {
157
- method: "POST",
158
- headers: { ...headers, ...paymentHeaders },
159
- body: body ?? undefined,
160
- });
161
- const data = await paidRes.text();
162
- if (!paidRes.ok) {
163
- console.error(`Paid request failed (${paidRes.status}): ${data}`);
164
- process.exit(1);
32
+ const parsed = {
33
+ code: parsePayErrorCode(body.code),
34
+ };
35
+ if (!isRecord(body.details)) {
36
+ return parsed;
165
37
  }
166
- console.log("Payment successful (crypto)");
38
+ const detailsRecord = body.details;
39
+ parsed.details = {
40
+ offered_price_usd6: readNumber(detailsRecord, "offered_price_usd6"),
41
+ offered_price_usd: readNumber(detailsRecord, "offered_price_usd"),
42
+ max_cost_usd6: readNumber(detailsRecord, "max_cost_usd6"),
43
+ max_cost_usd: readNumber(detailsRecord, "max_cost_usd"),
44
+ weekly_limit_usd6: readNumber(detailsRecord, "weekly_limit_usd6"),
45
+ weekly_limit_usd: readNumber(detailsRecord, "weekly_limit_usd"),
46
+ spent_this_week_usd6: readNumber(detailsRecord, "spent_this_week_usd6"),
47
+ spent_this_week_usd: readNumber(detailsRecord, "spent_this_week_usd"),
48
+ attempted_charge_usd6: readNumber(detailsRecord, "attempted_charge_usd6"),
49
+ attempted_charge_usd: readNumber(detailsRecord, "attempted_charge_usd"),
50
+ estimated_usd: readNumber(detailsRecord, "estimated_usd"),
51
+ amount_display: readString(detailsRecord, "amount_display"),
52
+ currency: readString(detailsRecord, "currency"),
53
+ };
54
+ return parsed;
55
+ }
56
+ export async function runPay(apiClient, url, options) {
57
+ const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
58
+ const method = normalizeMethod(options.method);
167
59
  try {
168
- console.log(JSON.stringify(JSON.parse(data), null, 2));
169
- }
170
- catch {
171
- console.log(data);
60
+ const response = await apiClient.pay(apiKey, {
61
+ url,
62
+ method,
63
+ headers: parseHeaders(options.header),
64
+ body: parseBody(options.body),
65
+ max_cost_usd: options.maxCost,
66
+ });
67
+ console.log(formatJson(response.body));
68
+ if (response.payment) {
69
+ console.log(`\nCharged: ${formatUsd(response.payment.charged_usd)} | Remaining: ${formatUsd(response.payment.remaining_budget_usd)}`);
70
+ }
172
71
  }
173
- }
174
- function registerPayCommand(program) {
175
- program
176
- .command("pay")
177
- .description("Pay a paywall-protected endpoint using card or crypto wallet")
178
- .argument("<url>", "URL of the paywall-protected endpoint")
179
- .option("--method <method>", "Payment method: card or crypto (default: auto-detect)")
180
- .option("--body <json>", "Request body JSON")
181
- .option("--header <key:value>", "Extra headers (repeatable)", (val, prev) => {
182
- prev.push(val);
183
- return prev;
184
- }, [])
185
- .action(async (url, opts) => {
186
- try {
187
- const extraHeaders = parseHeaders(opts.header);
188
- if (opts.method === "card") {
189
- const card = await readCardConfig();
190
- if (!card) {
191
- console.error("No card configured. Run: agentspend card setup");
192
- process.exit(1);
72
+ catch (error) {
73
+ if (error instanceof ApiError) {
74
+ const body = parsePayErrorBody(error.body);
75
+ if (error.status === 400 && body.code === "PRICE_EXCEEDS_MAX") {
76
+ const offered = body.details?.offered_price_usd ??
77
+ (typeof body.details?.offered_price_usd6 === "number" ? usd6ToUsd(body.details.offered_price_usd6) : 0);
78
+ const max = body.details?.max_cost_usd ??
79
+ (typeof body.details?.max_cost_usd6 === "number" ? usd6ToUsd(body.details.max_cost_usd6) : 0);
80
+ const estimatedUsd = body.details?.estimated_usd;
81
+ const amountDisplay = body.details?.amount_display;
82
+ const currency = body.details?.currency ?? "USDC";
83
+ console.error(`Price ${formatUsd(offered)} exceeds --max-cost ${formatUsd(max)}. Run without --max-cost or increase it.`);
84
+ if (amountDisplay) {
85
+ console.error(`Offered token amount: ${amountDisplay} ${currency} (~${formatUsdEstimate(estimatedUsd, offered)})`);
193
86
  }
194
- await payWithCard(url, card, opts.body, extraHeaders);
195
87
  return;
196
88
  }
197
- if (opts.method === "crypto") {
198
- const wallet = await readWalletConfig();
199
- if (!wallet) {
200
- console.error("No wallet configured. Run: agentspend wallet create");
201
- process.exit(1);
202
- }
203
- await payWithCrypto(url, wallet, opts.body, extraHeaders);
89
+ if (error.status === 400 && body.code === "PRICE_NOT_CONVERTIBLE") {
90
+ console.error("Price could not be converted to 6-decimal USD units for policy checks.");
204
91
  return;
205
92
  }
206
- // Auto-detect: try card first, then crypto
207
- const card = await readCardConfig();
208
- if (card) {
209
- await payWithCard(url, card, opts.body, extraHeaders);
93
+ if (error.status === 402 && body.code === "WEEKLY_BUDGET_EXCEEDED") {
94
+ const weeklyLimit = body.details?.weekly_limit_usd ??
95
+ (typeof body.details?.weekly_limit_usd6 === "number" ? usd6ToUsd(body.details.weekly_limit_usd6) : 0);
96
+ const spent = body.details?.spent_this_week_usd ??
97
+ (typeof body.details?.spent_this_week_usd6 === "number" ? usd6ToUsd(body.details.spent_this_week_usd6) : 0);
98
+ const attempted = body.details?.attempted_charge_usd ??
99
+ (typeof body.details?.attempted_charge_usd6 === "number" ? usd6ToUsd(body.details.attempted_charge_usd6) : 0);
100
+ console.error(`Weekly budget exceeded. Limit ${formatUsd(weeklyLimit)}, spent ${formatUsd(spent)}, attempted ${formatUsd(attempted)}.`);
210
101
  return;
211
102
  }
212
- const wallet = await readWalletConfig();
213
- if (wallet) {
214
- await payWithCrypto(url, wallet, opts.body, extraHeaders);
103
+ if (error.status === 403 && body.code === "DOMAIN_NOT_ALLOWLISTED") {
104
+ console.error("This domain is not in your AgentSpend allowlist.");
215
105
  return;
216
106
  }
217
- console.error("No payment method configured.");
218
- console.error("Set up a card: agentspend card setup");
219
- console.error("Or create a wallet: agentspend wallet create");
220
- process.exit(1);
221
107
  }
222
- catch (err) {
223
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
224
- process.exit(1);
225
- }
226
- });
108
+ throw error;
109
+ }
227
110
  }