agentspend 0.1.9 → 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 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,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
+ }