agentspend 0.2.0 → 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/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agentspend
3
- description: Find the right APIs for any task and pay for them, without authentication.
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
4
  ---
5
5
 
6
6
  # When To Use This Skill
@@ -9,6 +9,17 @@ Use this skill whenever the user asks to:
9
9
  - call an endpoint
10
10
  - fetch/search information outside local context
11
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 ...`
12
23
 
13
24
  ## Setup
14
25
 
@@ -58,10 +69,19 @@ npx agentspend check <url>
58
69
 
59
70
  Discover an endpoint's price without paying.
60
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
+
61
78
  **Example:**
62
79
 
63
80
  ```bash
64
- npx agentspend check <url>
81
+ npx agentspend check <url> \
82
+ --method POST \
83
+ --header "content-type:application/json" \
84
+ --body '{"key":"value"}'
65
85
  ```
66
86
 
67
87
  **Returns:**
@@ -1,9 +1,9 @@
1
1
  import { ApiError } from "../lib/api.js";
2
- import { requireApiKey } from "../lib/credentials.js";
2
+ import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
3
3
  import { formatUsd, usd6ToUsd } from "../lib/output.js";
4
4
  import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";
5
5
  export async function runCheck(apiClient, url, options) {
6
- const apiKey = await requireApiKey();
6
+ const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
7
7
  try {
8
8
  const response = await apiClient.check(apiKey, {
9
9
  url,
@@ -1,29 +1,6 @@
1
- import bcrypt from "bcryptjs";
2
- import crypto from "node:crypto";
3
1
  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
- }
2
+ import { clearPendingConfigureToken, readCredentials, savePendingConfigureToken, } from "../lib/credentials.js";
3
+ import { claimConfigureToken, getPendingConfigureStatus } from "../lib/auth-flow.js";
27
4
  async function tryAuthenticatedConfigure(apiClient, apiKey) {
28
5
  try {
29
6
  return await apiClient.configure(undefined, apiKey);
@@ -35,13 +12,6 @@ async function tryAuthenticatedConfigure(apiClient, apiKey) {
35
12
  throw error;
36
13
  }
37
14
  }
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
15
  export async function runConfigure(apiClient) {
46
16
  const credentials = await readCredentials();
47
17
  if (credentials) {
@@ -51,49 +21,24 @@ export async function runConfigure(apiClient) {
51
21
  return;
52
22
  }
53
23
  }
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.");
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}`);
80
28
  return;
81
29
  }
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.");
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.");
95
38
  }
96
- status = polled;
39
+ await clearPendingConfigureToken();
97
40
  }
98
- throw new Error("Configure timed out. Run agentspend configure again.");
41
+ const created = await apiClient.configure();
42
+ await savePendingConfigureToken(created.token);
43
+ console.log(`Open this URL to configure settings:\n${created.configure_url}`);
99
44
  }
@@ -1,5 +1,5 @@
1
1
  import { ApiError } from "../lib/api.js";
2
- import { requireApiKey } from "../lib/credentials.js";
2
+ import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
3
3
  import { formatJson, formatUsd, formatUsdEstimate, usd6ToUsd } from "../lib/output.js";
4
4
  import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";
5
5
  function isRecord(value) {
@@ -54,7 +54,7 @@ function parsePayErrorBody(body) {
54
54
  return parsed;
55
55
  }
56
56
  export async function runPay(apiClient, url, options) {
57
- const apiKey = await requireApiKey();
57
+ const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
58
58
  const method = normalizeMethod(options.method);
59
59
  try {
60
60
  const response = await apiClient.pay(apiKey, {
@@ -1,6 +1,6 @@
1
- import { requireApiKey } from "../lib/credentials.js";
1
+ import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
2
2
  export async function runSearch(apiClient, query) {
3
- const apiKey = await requireApiKey();
3
+ const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
4
4
  const response = await apiClient.search(apiKey, query);
5
5
  if (response.services.length === 0) {
6
6
  console.log(`No services matched "${response.query}".`);
@@ -1,7 +1,7 @@
1
- import { requireApiKey } from "../lib/credentials.js";
1
+ import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
2
2
  import { printStatus } from "../lib/output.js";
3
3
  export async function runStatus(apiClient) {
4
- const apiKey = await requireApiKey();
4
+ const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
5
5
  const status = await apiClient.status(apiKey);
6
6
  printStatus(status);
7
7
  }
@@ -0,0 +1,56 @@
1
+ import bcrypt from "bcryptjs";
2
+ import crypto from "node:crypto";
3
+ import { ApiError } from "./api.js";
4
+ import { clearPendingConfigureToken, readCredentials, readPendingConfigureToken, saveCredentials, } from "./credentials.js";
5
+ function generateApiKey() {
6
+ return `sk_agent_${crypto.randomBytes(32).toString("hex")}`;
7
+ }
8
+ async function getConfigureStatusOrNull(apiClient, token) {
9
+ try {
10
+ return await apiClient.configureStatus(token);
11
+ }
12
+ catch (error) {
13
+ if (error instanceof ApiError && (error.status === 401 || error.status === 404 || error.status === 410)) {
14
+ return null;
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+ export async function claimConfigureToken(apiClient, token) {
20
+ const apiKey = generateApiKey();
21
+ const apiKeyHash = await bcrypt.hash(apiKey, 12);
22
+ await apiClient.claimConfigure(token, apiKeyHash);
23
+ await saveCredentials(apiKey);
24
+ await clearPendingConfigureToken();
25
+ return apiKey;
26
+ }
27
+ export async function getPendingConfigureStatus(apiClient) {
28
+ const token = await readPendingConfigureToken();
29
+ if (!token) {
30
+ return null;
31
+ }
32
+ const status = await getConfigureStatusOrNull(apiClient, token);
33
+ if (!status || status.claim_status === "expired") {
34
+ await clearPendingConfigureToken();
35
+ return null;
36
+ }
37
+ return { token, status };
38
+ }
39
+ export async function resolveApiKeyWithAutoClaim(apiClient) {
40
+ const credentials = await readCredentials();
41
+ if (credentials) {
42
+ return credentials.api_key;
43
+ }
44
+ const pending = await getPendingConfigureStatus(apiClient);
45
+ if (!pending) {
46
+ throw new Error("No API key found. Run `agentspend configure` first.");
47
+ }
48
+ if (pending.status.claim_status === "ready_to_claim") {
49
+ return claimConfigureToken(apiClient, pending.token);
50
+ }
51
+ if (pending.status.claim_status === "awaiting_card") {
52
+ throw new Error(`Card setup required. Open this URL:\n${pending.status.configure_url}`);
53
+ }
54
+ await clearPendingConfigureToken();
55
+ throw new Error("Configure session already claimed. Run `agentspend configure` again.");
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentspend",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "AgentSpend CLI for managed x402 spending",
5
5
  "files": ["dist", "SKILL.md"],
6
6
  "type": "module",