agentspend 0.1.10 → 0.2.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 +20 -25
- package/SKILL.md +118 -0
- package/dist/cli.js +63 -0
- package/dist/commands/check.js +40 -0
- package/dist/commands/configure.js +99 -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/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,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentspend
|
|
3
|
+
description: Find the right APIs for any task and pay for them, without authentication.
|
|
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
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx agentspend configure
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Opens a URL to add a credit card and set a weekly spending limit. Saves credentials to `~/.agentspend/credentials.json`.
|
|
20
|
+
|
|
21
|
+
If already configured, re-running opens the dashboard to update settings.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
### Pay
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx agentspend pay <url>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Make a paid request. AgentSpend handles the payment automatically. Works with endpoints that support x402 (HTTP 402-based payment protocol for APIs).
|
|
32
|
+
|
|
33
|
+
**Options:**
|
|
34
|
+
- `--method <method>` — HTTP method (default: `GET`)
|
|
35
|
+
- `--body <body>` — Request body (JSON or text)
|
|
36
|
+
- `--header <header>` — Header in `key:value` format (repeatable)
|
|
37
|
+
- `--max-cost <usd>` — Maximum acceptable charge in USD (up to 6 decimal places)
|
|
38
|
+
|
|
39
|
+
**Returns:**
|
|
40
|
+
- Response body from the endpoint
|
|
41
|
+
- Charge amount and remaining weekly budget
|
|
42
|
+
|
|
43
|
+
**Example:**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx agentspend pay <url> \
|
|
47
|
+
--method POST \
|
|
48
|
+
--header "key:value" \
|
|
49
|
+
--body '{"key": "value"}' \
|
|
50
|
+
--max-cost 0.05
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Check
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx agentspend check <url>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Discover an endpoint's price without paying.
|
|
60
|
+
|
|
61
|
+
**Example:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx agentspend check <url>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Returns:**
|
|
68
|
+
- Price in USD
|
|
69
|
+
- Description (if available)
|
|
70
|
+
|
|
71
|
+
### Search
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx agentspend search <keywords>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Keyword search over service names and descriptions in the catalog. Returns up to 5 matching services.
|
|
78
|
+
|
|
79
|
+
**Example:**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx agentspend search "video generation"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Status
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx agentspend status
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Show account spending overview.
|
|
92
|
+
|
|
93
|
+
**Returns:**
|
|
94
|
+
- Weekly budget
|
|
95
|
+
- Amount spent this week
|
|
96
|
+
- Remaining budget
|
|
97
|
+
- Recent charges with amounts, domains, and timestamps
|
|
98
|
+
|
|
99
|
+
### Configure
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx agentspend configure
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Run onboarding or open the dashboard to update settings (weekly budget, domain allowlist, payment method).
|
|
106
|
+
|
|
107
|
+
## Spending Controls
|
|
108
|
+
|
|
109
|
+
- **Weekly budget** — Set during configure. Requests that would exceed the budget are rejected.
|
|
110
|
+
- **Per-request max cost** — Use `--max-cost` on `pay` to reject requests above a price threshold.
|
|
111
|
+
- **Domain allowlist** — Configurable via the dashboard. Requests to non-allowlisted domains are rejected.
|
|
112
|
+
|
|
113
|
+
## Common Errors
|
|
114
|
+
|
|
115
|
+
- **`WEEKLY_BUDGET_EXCEEDED`** — Weekly spending limit reached. Run `npx agentspend configure` to increase the budget.
|
|
116
|
+
- **`DOMAIN_NOT_ALLOWLISTED`** — The target domain is not in the allowlist. Run `npx agentspend configure` to update allowed domains.
|
|
117
|
+
- **`PRICE_EXCEEDS_MAX`** — Endpoint price is higher than `--max-cost`. Increase the value or remove the flag.
|
|
118
|
+
- **`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 { requireApiKey } from "../lib/credentials.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 requireApiKey();
|
|
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,99 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { ApiError } from "../lib/api.js";
|
|
4
|
+
import { clearPendingConfigureToken, readCredentials, readPendingConfigureToken, saveCredentials, savePendingConfigureToken, } from "../lib/credentials.js";
|
|
5
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
6
|
+
const CONFIGURE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
function generateApiKey() {
|
|
11
|
+
return `sk_agent_${crypto.randomBytes(32).toString("hex")}`;
|
|
12
|
+
}
|
|
13
|
+
function isExpiredStatus(status) {
|
|
14
|
+
return status.claim_status === "expired";
|
|
15
|
+
}
|
|
16
|
+
async function getStatusOrNull(apiClient, token) {
|
|
17
|
+
try {
|
|
18
|
+
return await apiClient.configureStatus(token);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 404 || error.status === 410)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function tryAuthenticatedConfigure(apiClient, apiKey) {
|
|
28
|
+
try {
|
|
29
|
+
return await apiClient.configure(undefined, apiKey);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof ApiError && error.status === 401) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function claimAndSaveCredentials(apiClient, token) {
|
|
39
|
+
const apiKey = generateApiKey();
|
|
40
|
+
const apiKeyHash = await bcrypt.hash(apiKey, 12);
|
|
41
|
+
await apiClient.claimConfigure(token, apiKeyHash);
|
|
42
|
+
await saveCredentials(apiKey);
|
|
43
|
+
await clearPendingConfigureToken();
|
|
44
|
+
}
|
|
45
|
+
export async function runConfigure(apiClient) {
|
|
46
|
+
const credentials = await readCredentials();
|
|
47
|
+
if (credentials) {
|
|
48
|
+
const authenticatedResponse = await tryAuthenticatedConfigure(apiClient, credentials.api_key);
|
|
49
|
+
if (authenticatedResponse) {
|
|
50
|
+
console.log(`Open this URL to configure settings:\n${authenticatedResponse.configure_url}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let token = await readPendingConfigureToken();
|
|
55
|
+
let status = null;
|
|
56
|
+
if (token) {
|
|
57
|
+
status = await getStatusOrNull(apiClient, token);
|
|
58
|
+
if (!status || isExpiredStatus(status)) {
|
|
59
|
+
token = null;
|
|
60
|
+
status = null;
|
|
61
|
+
await clearPendingConfigureToken();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!token) {
|
|
65
|
+
const created = await apiClient.configure();
|
|
66
|
+
token = created.token;
|
|
67
|
+
await savePendingConfigureToken(token);
|
|
68
|
+
status = created;
|
|
69
|
+
}
|
|
70
|
+
if (!status) {
|
|
71
|
+
throw new Error("Unable to initialize configure session. Run agentspend configure again.");
|
|
72
|
+
}
|
|
73
|
+
console.log(`Open this URL to configure settings:\n${status.configure_url}\n`);
|
|
74
|
+
console.log("Waiting for card setup and API key claim...");
|
|
75
|
+
const started = Date.now();
|
|
76
|
+
while (Date.now() - started < CONFIGURE_TIMEOUT_MS) {
|
|
77
|
+
if (status.claim_status === "ready_to_claim") {
|
|
78
|
+
await claimAndSaveCredentials(apiClient, token);
|
|
79
|
+
console.log("Ready! Your agent can now spend.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (status.claim_status === "expired") {
|
|
83
|
+
await clearPendingConfigureToken();
|
|
84
|
+
throw new Error("Configure session expired. Run agentspend configure again.");
|
|
85
|
+
}
|
|
86
|
+
if (status.claim_status === "claimed") {
|
|
87
|
+
await clearPendingConfigureToken();
|
|
88
|
+
throw new Error("Configure session already claimed. Run agentspend configure again.");
|
|
89
|
+
}
|
|
90
|
+
await sleep(POLL_INTERVAL_MS);
|
|
91
|
+
const polled = await getStatusOrNull(apiClient, token);
|
|
92
|
+
if (!polled) {
|
|
93
|
+
await clearPendingConfigureToken();
|
|
94
|
+
throw new Error("Configure session expired. Run agentspend configure again.");
|
|
95
|
+
}
|
|
96
|
+
status = polled;
|
|
97
|
+
}
|
|
98
|
+
throw new Error("Configure timed out. Run agentspend configure again.");
|
|
99
|
+
}
|