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 +20 -25
- package/SKILL.md +138 -0
- package/dist/cli.js +63 -0
- package/dist/commands/check.js +40 -0
- package/dist/commands/configure.js +44 -0
- package/dist/commands/pay.js +89 -206
- package/dist/commands/search.js +16 -0
- package/dist/commands/setup.js +36 -0
- package/dist/commands/status.js +7 -0
- package/dist/dev-index.js +10 -0
- package/dist/index.js +5 -15
- package/dist/lib/api.js +98 -0
- package/dist/lib/auth-flow.js +56 -0
- package/dist/lib/credentials.js +65 -0
- package/dist/lib/output.js +76 -0
- package/dist/lib/request-options.js +35 -0
- package/dist/types.js +1 -0
- package/package.json +17 -18
- package/dist/commands/card.d.ts +0 -2
- package/dist/commands/card.js +0 -179
- package/dist/commands/crypto-wallet.d.ts +0 -2
- package/dist/commands/crypto-wallet.js +0 -113
- package/dist/commands/pay.d.ts +0 -2
- package/dist/commands/wallet.d.ts +0 -2
- package/dist/commands/wallet.js +0 -102
- package/dist/index.d.ts +0 -2
- package/src/commands/card.ts +0 -230
- package/src/commands/pay.ts +0 -270
- package/src/commands/wallet.ts +0 -118
- package/src/index.ts +0 -19
- package/tsconfig.json +0 -8
package/README.md
CHANGED
|
@@ -1,43 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agentspend
|
|
2
2
|
|
|
3
|
-
CLI for
|
|
3
|
+
AgentSpend CLI for calling x402 endpoints through AgentSpend Cloud.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Commands
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
|
-
|
|
15
|
-
agentspend
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
+
## Credentials
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Credentials are stored at `~/.agentspend/credentials.json`.
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## Local backend dev CLI
|
|
26
26
|
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
29
|
+
```bash
|
|
30
|
+
bun run dev:local -- configure
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
## Configuration
|
|
33
|
+
Build a non-published local binary:
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
package/dist/commands/pay.js
CHANGED
|
@@ -1,227 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
111
|
-
console.error(`Request failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
112
|
-
process.exit(1);
|
|
26
|
+
return undefined;
|
|
113
27
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 (
|
|
198
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
227
110
|
}
|