Skill: jpbonch/agentspend (version 0.1.3)

--- name: agentspend 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, 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. --- # When To Use This Skill Use this skill whenever the user asks to: - find data from external APIs - call an endpoint - fetch/search information outside local context - generate an image, video, voice, transcription, or music with an external API - scrape/extract data from a URL - find an API for a task ("is there an API for X?") If the task needs an external paid API, start with `agentspend search`. ## Playbook (Default Workflow) 1. `npx agentspend search "<task>"` 2. Confirm cost and constraints with the user (`--max-cost`, budget, allowlist) 3. `npx agentspend pay <endpoint> --method ... --header ... --body ... --max-cost ...` ## Setup ```bash npx agentspend configure ``` Opens a URL to add a credit card and set a weekly spending limit. Saves credentials to `~/.agentspend/credentials.json`. If already configured, re-running opens the dashboard to update settings. ## Commands ### Pay ```bash npx agentspend pay <url> ``` Make a paid request. AgentSpend handles the payment automatically. **Options:** - `--method <method>` — HTTP method (default: `GET`) - `--body <body>` — Request body (JSON or text) - `--header <header>` — Header in `key:value` format (repeatable) - `--max-cost <usd>` — Maximum acceptable charge in USD (up to 6 decimal places) **Returns:** - Response body from the endpoint - Charge amount and remaining weekly budget **Example:** ```bash npx agentspend pay <url> \ --method POST \ --header "key:value" \ --body '{"key": "value"}' \ --max-cost 0.05 ``` ### Check ```bash npx agentspend check <url> ``` Discover an endpoint's price without paying. Important: - `check` must use the same request shape you plan to `pay` with. - Always pass `--method` for non-GET endpoints. - If the endpoint needs headers/body, include the same `--header` and `--body` on `check`. - If request shape is wrong, endpoint may return `404`/`400` instead of `402`, and no price can be extracted. **Example:** ```bash npx agentspend check <url> \ --method POST \ --header "content-type:application/json" \ --body '{"key":"value"}' ``` **Returns:** - Price in USD - Description (if available) ### Search ```bash npx agentspend search <keywords> ``` Keyword search over service names and descriptions in the catalog. Returns up to 5 matching services. **Example:** ```bash npx agentspend search "video generation" ``` ### Status ```bash npx agentspend status ``` Show account spending overview. **Returns:** - Weekly budget - Amount spent this week - Remaining budget - Recent charges with amounts, domains, and timestamps ### Configure ```bash npx agentspend configure ``` Run onboarding or open the dashboard to update settings (weekly budget, domain allowlist, payment method). ## Spending Controls - **Weekly budget** — Set during configure. Requests that would exceed the budget are rejected. - **Per-request max cost** — Use `--max-cost` on `pay` to reject requests above a price threshold. - **Domain allowlist** — Configurable via the dashboard. Requests to non-allowlisted domains are rejected. ## Common Errors - **`WEEKLY_BUDGET_EXCEEDED`** — Weekly spending limit reached. Run `npx agentspend configure` to increase the budget. - **`DOMAIN_NOT_ALLOWLISTED`** — The target domain is not in the allowlist. Run `npx agentspend configure` to update allowed domains. - **`PRICE_EXCEEDS_MAX`** — Endpoint price is higher than `--max-cost`. Increase the value or remove the flag.

path: README.md
content: # agentspend

AgentSpend CLI for calling x402 endpoints through AgentSpend Cloud.

## Install

```bash
npm install
npm run build
```

## Commands

```bash
agentspend configure
agentspend pay <url> [--method GET|POST|PUT|PATCH|DELETE|...] [--body '{"hello":"world"}'] [--header 'Content-Type:application/json'] [--max-cost 5.000000]
agentspend check <url> [--method GET|POST|PUT|PATCH|DELETE|...] [--body '{"hello":"world"}'] [--header 'Content-Type:application/json']
agentspend status
```

## Credentials

Credentials are stored at `~/.agentspend/credentials.json`.

## Local backend dev CLI

Use the local entrypoint (hardcoded to `http://127.0.0.1:8787`) when testing against a local backend:

```bash
bun run dev:local -- configure
```

Build a non-published local binary:

```bash
bun run build
node dist/dev-index.js configure
```


path: package.json
content: {
  "name": "agentspend",
  "version": "0.2.2",
  "description": "AgentSpend CLI for managed x402 spending",
  "files": ["dist", "SKILL.md"],
  "type": "module",
  "bin": {
    "agentspend": "dist/index.js"
  },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "dev": "tsx src/index.ts",
    "dev:local": "tsx src/dev-index.ts",
    "typecheck": "tsc -p tsconfig.json --noEmit"
  },
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "commander": "^12.1.0"
  },
  "devDependencies": {
    "@types/bcryptjs": "^2.4.6",
    "@types/node": "^22.10.2",
    "tsx": "^4.19.2",
    "typescript": "^5.7.2"
  }
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist",
    "rootDir": "src",
    "resolveJsonModule": true
  },
  "include": ["src/**/*.ts"]
}


path: src/dev-index.ts
content: #!/usr/bin/env node
import { runCli } from "./cli.js";

const LOCAL_API_BASE_URL = "http://127.0.0.1:8787";

runCli({
  baseUrl: LOCAL_API_BASE_URL,
  programName: "agentspend-dev",
}).catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exitCode = 1;
});


path: src/cli.ts
content: import { Command } from "commander";
import { runCheck } from "./commands/check.js";
import { runConfigure } from "./commands/configure.js";
import { runPay } from "./commands/pay.js";
import { runSearch } from "./commands/search.js";
import { runStatus } from "./commands/status.js";
import { AgentspendApiClient } from "./lib/api.js";

function parsePositiveUsd(value: string): number {
  const parsed = Number(value);

  if (!Number.isFinite(parsed) || parsed <= 0) {
    throw new Error(`Expected a positive USD value, received: ${value}`);
  }

  const rounded = Math.round(parsed * 1_000_000) / 1_000_000;
  if (Math.abs(parsed - rounded) > 1e-9) {
    throw new Error(`Expected at most 6 decimal places, received: ${value}`);
  }

  return parsed;
}

export async function runCli(options?: { baseUrl?: string; programName?: string }): Promise<void> {
  const program = new Command();
  const apiClient = new AgentspendApiClient(options?.baseUrl);
  const programName = options?.programName ?? "agentspend";

  program.name(programName).description("AgentSpend CLI").version("0.2.0");

  program
    .command("pay")
    .argument("<url>", "URL to call")
    .description("Make a paid request")
    .option("-X, --method <method>", "HTTP method for target request", "GET")
    .option("--body <body>", "Request body (JSON or text)")
    .option("--header <header>", "Header in key:value form", (value, previous: string[]) => {
      return [...previous, value];
    }, [])
    .option("--max-cost <usd>", "Maximum acceptable charge in USD (up to 6 decimals)", parsePositiveUsd)
    .action(async (url: string, commandOptions: { method?: string; body?: string; header?: string[]; maxCost?: number }) => {
      await runPay(apiClient, url, commandOptions);
    });

  program
    .command("check")
    .argument("<url>", "URL to check")
    .description("Discover endpoint price without paying")
    .option("-X, --method <method>", "HTTP method for target request", "GET")
    .option("--body <body>", "Request body (JSON or text)")
    .option("--header <header>", "Header in key:value form", (value, previous: string[]) => {
      return [...previous, value];
    }, [])
    .action(async (url: string, commandOptions: { method?: string; body?: string; header?: string[] }) => {
      await runCheck(apiClient, url, commandOptions);
    });

  program
    .command("search")
    .argument("<query...>", "Keyword query")
    .description("Search services by name and description")
    .action(async (queryParts: string[]) => {
      await runSearch(apiClient, queryParts.join(" "));
    });

  program.command("status").description("Show weekly budget and recent charges").action(async () => {
    await runStatus(apiClient);
  });

  program.command("configure").description("Run onboarding/configuration flow").action(async () => {
    await runConfigure(apiClient);
  });

  await program.parseAsync(process.argv);
}


path: src/types.ts
content: export interface Credentials {
  [REDACTED_SECRET];
  created_at: string;
}

export interface PayRequest {
  url: string;
  method?: string;
  headers?: Record<string, string>;
  body?: unknown;
  max_cost_usd?: number;
}

export interface PayResponse {
  status: number;
  headers: Record<string, string>;
  body: unknown;
  payment: {
    charged_usd6: number;
    charged_usd: number;
    charged_amount_minor: number | null;
    charged_amount_display: string | null;
    charged_currency: string;
    estimated_usd: number | null;
    pricing_note: string | null;
    remaining_budget_usd6: number;
    remaining_budget_usd: number;
    transaction_hash: string | null;
    paid_to: string | null;
    network: string | null;
    scheme: string | null;
    resource: string | null;
  } | null;
}

export interface CheckRequest {
  url: string;
  method?: string;
  headers?: Record<string, string>;
  body?: unknown;
}

export interface CheckResponse {
  free?: boolean;
  price_usd6?: number | null;
  price_usd?: number | null;
  estimated_usd?: number | null;
  currency?: string;
  pay_to?: string | null;
  amount_minor?: number | null;
  amount_display?: string | null;
  decimals?: number | null;
  description?: string | null;
  network?: string | null;
  scheme?: string | null;
  resource?: string | null;
  max_timeout_seconds?: number | null;
  mime_type?: string | null;
  pricing_note?: string | null;
  status?: number;
}

export interface StatusCharge {
  id: string;
  amount_usd6: number;
  amount_usd?: number;
  target_domain: string;
  target_url: string;
  target_status: number | null;
  created_at: string;
}

export interface StatusResponse {
  weekly_budget_usd6: number;
  weekly_budget_usd: number;
  spent_this_week_usd6: number;
  spent_this_week_usd: number;
  remaining_budget_usd6: number;
  remaining_budget_usd: number;
  recent_charges: StatusCharge[];
}

export type ConfigureClaimStatus = "awaiting_card" | "ready_to_claim" | "claimed" | "expired";

export interface ConfigureStatusResponse {
  [REDACTED_SECRET];
  configure_url: string;
  claim_status: ConfigureClaimStatus;
  has_card_on_file: boolean;
  has_api_key: boolean;
  weekly_limit_usd6: number | null;
  weekly_limit_usd: number | null;
}

export interface ConfigureClaimResponse {
  status: "claimed";
  agent_id: string;
}

export interface SearchService {
  id: string;
  name: string;
  description: string;
  domain: string;
  skill_url: string | null;
}

export interface SearchResponse {
  query: string;
  services: SearchService[];
}


path: src/index.ts
content: #!/usr/bin/env node
import { runCli } from "./cli.js";

runCli().catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exitCode = 1;
});


path: src/commands/status.ts
content: import { AgentspendApiClient } from "../lib/api.js";
import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
import { printStatus } from "../lib/output.js";

export async function runStatus(apiClient: AgentspendApiClient): Promise<void> {
  const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
  const status = await apiClient.status(apiKey);
  printStatus(status);
}


path: src/commands/search.ts
content: import { AgentspendApiClient } from "../lib/api.js";
import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";

export async function runSearch(apiClient: AgentspendApiClient, query: string): Promise<void> {
  const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
  const response = await apiClient.search(apiKey, query);

  if (response.services.length === 0) {
    console.log(`No services matched "${response.query}".`);
    return;
  }

  for (const service of response.services) {
    console.log(service.name);
    console.log(`Description: ${service.description}`);
    console.log(`Domain: ${service.domain}`);
    console.log(`Skill URL: ${service.skill_url ?? "n/a"}`);
    console.log("");
  }
}


path: src/commands/pay.ts
content: import { ApiError, AgentspendApiClient } from "../lib/api.js";
import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
import { formatJson, formatUsd, formatUsdEstimate, usd6ToUsd } from "../lib/output.js";
import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";

export interface PayCommandOptions {
  method?: string;
  body?: string;
  header?: string[];
  maxCost?: number;
}

type PayErrorCode =
  | "PRICE_EXCEEDS_MAX"
  | "PRICE_NOT_CONVERTIBLE"
  | "WEEKLY_BUDGET_EXCEEDED"
  | "DOMAIN_NOT_ALLOWLISTED";

interface PayErrorDetails {
  offered_price_usd6?: number;
  offered_price_usd?: number;
  max_cost_usd6?: number;
  max_cost_usd?: number;
  weekly_limit_usd6?: number;
  weekly_limit_usd?: number;
  spent_this_week_usd6?: number;
  spent_this_week_usd?: number;
  attempted_charge_usd6?: number;
  attempted_charge_usd?: number;
  estimated_usd?: number;
  amount_display?: string;
  currency?: string;
}

interface ParsedPayErrorBody {
  code?: PayErrorCode;
  details?: PayErrorDetails;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function readNumber(record: Record<string, unknown>, key: string): number | undefined {
  const value = record[key];
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}

function readString(record: Record<string, unknown>, key: string): string | undefined {
  const value = record[key];
  return typeof value === "string" ? value : undefined;
}

function parsePayErrorCode(value: unknown): PayErrorCode | undefined {
  if (typeof value !== "string") {
    return undefined;
  }

  if (
    value === "PRICE_EXCEEDS_MAX" ||
    value === "PRICE_NOT_CONVERTIBLE" ||
    value === "WEEKLY_BUDGET_EXCEEDED" ||
    value === "DOMAIN_NOT_ALLOWLISTED"
  ) {
    return value;
  }

  return undefined;
}

function parsePayErrorBody(body: unknown): ParsedPayErrorBody {
  if (!isRecord(body)) {
    return {};
  }

  const parsed: ParsedPayErrorBody = {
    code: parsePayErrorCode(body.code),
  };

  if (!isRecord(body.details)) {
    return parsed;
  }

  const detailsRecord = body.details;
  parsed.details = {
    offered_price_usd6: readNumber(detailsRecord, "offered_price_usd6"),
    offered_price_usd: readNumber(detailsRecord, "offered_price_usd"),
    max_cost_usd6: readNumber(detailsRecord, "max_cost_usd6"),
    max_cost_usd: readNumber(detailsRecord, "max_cost_usd"),
    weekly_limit_usd6: readNumber(detailsRecord, "weekly_limit_usd6"),
    weekly_limit_usd: readNumber(detailsRecord, "weekly_limit_usd"),
    spent_this_week_usd6: readNumber(detailsRecord, "spent_this_week_usd6"),
    spent_this_week_usd: readNumber(detailsRecord, "spent_this_week_usd"),
    attempted_charge_usd6: readNumber(detailsRecord, "attempted_charge_usd6"),
    attempted_charge_usd: readNumber(detailsRecord, "attempted_charge_usd"),
    estimated_usd: readNumber(detailsRecord, "estimated_usd"),
    amount_display: readString(detailsRecord, "amount_display"),
    currency: readString(detailsRecord, "currency"),
  };

  return parsed;
}

export async function runPay(apiClient: AgentspendApiClient, url: string, options: PayCommandOptions): Promise<void> {
  const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
  const method = normalizeMethod(options.method);

  try {
    const response = await apiClient.pay(apiKey, {
      url,
      method,
      headers: parseHeaders(options.header),
      body: parseBody(options.body),
      max_cost_usd: options.maxCost,
    });

    console.log(formatJson(response.body));

    if (response.payment) {
      console.log(
        `\nCharged: ${formatUsd(response.payment.charged_usd)} | Remaining: ${formatUsd(response.payment.remaining_budget_usd)}`,
      );
    }
  } catch (error) {
    if (error instanceof ApiError) {
      const body = parsePayErrorBody(error.body);

      if (error.status === 400 && body.code === "PRICE_EXCEEDS_MAX") {
        const offered =
          body.details?.offered_price_usd ??
          (typeof body.details?.offered_price_usd6 === "number" ? usd6ToUsd(body.details.offered_price_usd6) : 0);
        const max =
          body.details?.max_cost_usd ??
          (typeof body.details?.max_cost_usd6 === "number" ? usd6ToUsd(body.details.max_cost_usd6) : 0);
        const estimatedUsd = body.details?.estimated_usd;
        const amountDisplay = body.details?.amount_display;
        const currency = body.details?.currency ?? "USDC";

        console.error(
          `Price ${formatUsd(offered)} exceeds --max-cost ${formatUsd(max)}. Run without --max-cost or increase it.`,
        );

        if (amountDisplay) {
          console.error(
            `Offered token amount: ${amountDisplay} ${currency} (~${formatUsdEstimate(estimatedUsd, offered)})`,
          );
        }

        return;
      }

      if (error.status === 400 && body.code === "PRICE_NOT_CONVERTIBLE") {
        console.error("Price could not be converted to 6-decimal USD units for policy checks.");
        return;
      }

      if (error.status === 402 && body.code === "WEEKLY_BUDGET_EXCEEDED") {
        const weeklyLimit =
          body.details?.weekly_limit_usd ??
          (typeof body.details?.weekly_limit_usd6 === "number" ? usd6ToUsd(body.details.weekly_limit_usd6) : 0);
        const spent =
          body.details?.spent_this_week_usd ??
          (typeof body.details?.spent_this_week_usd6 === "number" ? usd6ToUsd(body.details.spent_this_week_usd6) : 0);
        const attempted =
          body.details?.attempted_charge_usd ??
          (typeof body.details?.attempted_charge_usd6 === "number" ? usd6ToUsd(body.details.attempted_charge_usd6) : 0);

        console.error(
          `Weekly budget exceeded. Limit ${formatUsd(weeklyLimit)}, spent ${formatUsd(spent)}, attempted ${formatUsd(attempted)}.`,
        );
        return;
      }

      if (error.status === 403 && body.code === "DOMAIN_NOT_ALLOWLISTED") {
        console.error("This domain is not in your AgentSpend allowlist.");
        return;
      }
    }

    throw error;
  }
}


path: src/commands/configure.ts
content: import { ApiError, AgentspendApiClient } from "../lib/api.js";
import {
  clearPendingConfigureToken,
  readCredentials,
  savePendingConfigureToken,
} from "../lib/credentials.js";
import { claimConfigureToken, getPendingConfigureStatus } from "../lib/auth-flow.js";
import type { ConfigureStatusResponse } from "../types.js";

async function tryAuthenticatedConfigure(
  apiClient: AgentspendApiClient,
  [REDACTED_SECRET],
): Promise<ConfigureStatusResponse | null> {
  try {
    return await apiClient.configure(undefined, apiKey);
  } catch (error) {
    if (error instanceof ApiError && error.status === 401) {
      return null;
    }

    throw error;
  }
}

export async function runConfigure(apiClient: AgentspendApiClient): Promise<void> {
  const credentials = await readCredentials();

  if (credentials) {
    const authenticatedResponse = await tryAuthenticatedConfigure(apiClient, credentials.api_key);
    if (authenticatedResponse) {
      console.log(`Open this URL to configure settings:\n${authenticatedResponse.configure_url}`);
      return;
    }
  }

  const pending = await getPendingConfigureStatus(apiClient);

  if (pending) {
    if (pending.status.claim_status === "awaiting_card") {
      console.log(`Open this URL to configure settings:\n${pending.status.configure_url}`);
      return;
    }

    if (pending.status.claim_status === "ready_to_claim") {
      const apiKey = await claimConfigureToken(apiClient, pending.token);
      const claimedResponse = await tryAuthenticatedConfigure(apiClient, apiKey);

      if (claimedResponse) {
        console.log(`Open this URL to configure settings:\n${claimedResponse.configure_url}`);
        return;
      }

      throw new Error("API key was claimed, but configure session could not be created. Run agentspend configure again.");
    }

    await clearPendingConfigureToken();
  }

  const created = await apiClient.configure();
  await savePendingConfigureToken(created.token);
  console.log(`Open this URL to configure settings:\n${created.configure_url}`);
}


path: src/commands/check.ts
content: import { ApiError, AgentspendApiClient } from "../lib/api.js";
import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
import { formatUsd, usd6ToUsd } from "../lib/output.js";
import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";

export interface CheckCommandOptions {
  method?: string;
  body?: string;
  header?: string[];
}

export async function runCheck(
  apiClient: AgentspendApiClient,
  url: string,
  options: CheckCommandOptions,
): Promise<void> {
  const apiKey = await resolveApiKeyWithAutoClaim(apiClient);

  try {
    const response = await apiClient.check(apiKey, {
      url,
      method: normalizeMethod(options.method),
      headers: parseHeaders(options.header),
      body: parseBody(options.body),
    });

    if (response.free) {
      if ((response.status ?? 200) >= 400) {
        console.log(`No payment required, but endpoint returned status ${response.status}.`);
        return;
      }

      console.log("This endpoint is free.");
      return;
    }

    const policyUsd =
      response.price_usd ?? (typeof response.price_usd6 === "number" ? usd6ToUsd(response.price_usd6) : null);
    const price = policyUsd !== null ? formatUsd(policyUsd) : "unavailable";
    const description = response.description ?? "unavailable";
    console.log(`Price: ${price}`);
    console.log(`Description: ${description}`);

    return;
  } catch (error) {
    if (error instanceof ApiError) {
      const body = error.body as { code?: string; details?: { header_names?: string[] } };
      if (error.status === 502 && body?.code === "UNSUPPORTED_PAYMENT_REQUIRED_FORMAT") {
        const headers = (body.details?.header_names ?? []).join(", ");
        console.error(
          `Endpoint returned 402 with an unsupported payment format. Headers seen: ${headers || "none"}.`,
        );
        return;
      }
    }

    throw error;
  }
}


path: src/lib/output.ts
content: import type { StatusCharge, StatusResponse } from "../types.js";

const USD6_SCALE = 1_000_000;

function formatUsdNumber(value: number, minDecimals = 2, maxDecimals = 6): string {
  const fixed = value.toFixed(maxDecimals);
  const trimmed = fixed.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");

  if (!trimmed.includes(".")) {
    return minDecimals > 0 ? `${trimmed}.${"0".repeat(minDecimals)}` : trimmed;
  }

  const [whole, frac = ""] = trimmed.split(".");
  if (frac.length >= minDecimals) {
    return trimmed;
  }

  return `${whole}.${frac.padEnd(minDecimals, "0")}`;
}

export function usd6ToUsd(usd6: number): number {
  return usd6 / USD6_SCALE;
}

export function formatUsd(usd: number): string {
  if (!Number.isFinite(usd)) {
    return "unknown";
  }

  return `$${formatUsdNumber(usd, 2, 6)}`;
}

export function formatUsdEstimate(estimatedUsd: number | null | undefined, fallbackUsd?: number | null): string {
  if (typeof estimatedUsd === "number") {
    return formatUsd(estimatedUsd);
  }

  if (typeof fallbackUsd === "number") {
    return formatUsd(fallbackUsd);
  }

  return "unknown";
}

export function formatJson(value: unknown): string {
  if (typeof value === "string") {
    return value;
  }

  return JSON.stringify(value, null, 2);
}

export function relativeTime(iso: string): string {
  const timestamp = new Date(iso).getTime();
  const diffMs = Date.now() - timestamp;
  const abs = Math.max(0, Math.floor(diffMs / 1000));

  if (abs < 60) {
    return `${abs}s ago`;
  }

  const minutes = Math.floor(abs / 60);
  if (minutes < 60) {
    return `${minutes}m ago`;
  }

  const hours = Math.floor(minutes / 60);
  if (hours < 24) {
    return `${hours}h ago`;
  }

  const days = Math.floor(hours / 24);
  return `${days}d ago`;
}

function formatCharge(charge: StatusCharge): string {
  const amountUsd = charge.amount_usd ?? usd6ToUsd(charge.amount_usd6);
  const amount = formatUsd(amountUsd).padEnd(10);
  const domain = charge.target_domain.padEnd(24);
  const when = relativeTime(charge.created_at);

  return `  ${amount} ${domain} ${when}`;
}

export function printStatus(status: StatusResponse): void {
  console.log(`Weekly budget:   ${formatUsd(status.weekly_budget_usd)}`);
  console.log(`Spent this week: ${formatUsd(status.spent_this_week_usd)}`);
  console.log(`Remaining:       ${formatUsd(status.remaining_budget_usd)}`);
  console.log("");

  if (status.recent_charges.length === 0) {
    console.log("Recent charges: none");
    return;
  }

  console.log("Recent charges:");
  for (const charge of status.recent_charges) {
    console.log(formatCharge(charge));
  }
}


path: src/lib/request-options.ts
content: const METHOD_TOKEN_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;

export function normalizeMethod(method: string | undefined): string {
  const normalized = (method ?? "GET").trim().toUpperCase();

  if (!normalized || !METHOD_TOKEN_PATTERN.test(normalized)) {
    throw new Error(`Invalid HTTP method: ${method ?? ""}`);
  }

  return normalized;
}

export function parseHeaders(rawHeaders: string[] | undefined): Record<string, string> {
  const parsed: Record<string, string> = {};

  for (const header of rawHeaders ?? []) {
    const separator = header.indexOf(":");
    if (separator === -1) {
      throw new Error(`Invalid header format: ${header}. Expected key:value.`);
    }

    const key = header.slice(0, separator).trim();
    const value = header.slice(separator + 1).trim();

    if (!key || !value) {
      throw new Error(`Invalid header format: ${header}. Expected key:value.`);
    }

    parsed[key] = value;
  }

  return parsed;
}

export function parseBody(rawBody: string | undefined): unknown {
  if (rawBody === undefined) {
    return undefined;
  }

  try {
    return JSON.parse(rawBody);
  } catch {
    return rawBody;
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)