diff --git a/README.md b/README.md index a8977c8..d03e746 100644 --- a/README.md +++ b/README.md @@ -64,17 +64,17 @@ acp agent create # creates the agent identity + EVM wallet `acp configure` **opens a browser and needs an interactive human session** — run it once on a workstation; the saved token is reusable. -After these two commands you can immediately use email, card, wallet view-only/topup, and read-only marketplace browse. Anything that signs on-chain (wallet sign/send, tokenization, marketplace job actions) additionally needs `acp agent add-signer` — covered in the [Wallet](#wallet) section. +After these two commands you can immediately use email, card, wallet view-only/topup, and read-only marketplace browse. Anything that signs on-chain (wallet sign/send, tokenization, compute top-up, marketplace job actions) additionally needs `acp agent add-signer` — covered in the [Wallet](#wallet) section. ### Environment variables All optional. The CLI works out of the box after `acp configure`. -| Variable | Default | Purpose | -|---|---|---| -| `ACP_CONFIG_DIR` | `~/.config/acp` | Where `acp configure` saves config. | -| `IS_TESTNET` | `false` | Set to `true` for testnet chains, API, and Privy app. Global toggle. | -| `PARTNER_ID` | — | Partner ID for `acp agent tokenize` only. | +| Variable | Default | Purpose | +| ---------------- | --------------- | -------------------------------------------------------------------- | +| `ACP_CONFIG_DIR` | `~/.config/acp` | Where `acp configure` saves config. | +| `IS_TESTNET` | `false` | Set to `true` for testnet chains, API, and Privy app. Global toggle. | +| `PARTNER_ID` | — | Partner ID for `acp agent tokenize` only. | Mainnet and testnet keep state in separate files (`config.json` vs `config-testnet.json`) so identities don't mix when toggling `IS_TESTNET`. @@ -89,7 +89,7 @@ acp [options] [--json] Sections below are grouped by pillar: - **Shared** — [Agent Management](#agent-management), [Tokenization](#tokenization), [Chain Info](#chain-info) -- **Identity** — [Wallet](#wallet), [Agent Email](#agent-email), [Agent Card](#agent-card) +- **Identity** — [Wallet](#wallet), [Agent Email](#agent-email), [Agent Card](#agent-card), [Compute](#compute) - **Commerce** — [Browsing Agents](#browsing-agents), [Offering Management](#offering-management), [Subscription Management](#subscription-management), [Resource Management](#resource-management), [Client Commands](#client-commands), [Provider Commands](#provider-commands), [Job Queries](#job-queries), [Messaging](#messaging), [Event Streaming](#event-streaming) ### Agent Management @@ -326,6 +326,18 @@ acp card 3ds > *may* still return them while the spend-request is active, but they're > absent after capture or expiry — don't rely on `get` for re-fetch. +### Compute + +The compute account holds a USDC-denominated balance that's drawn down as the agent runs its own LLM-inference workloads. + +```bash +# Show the compute account balance, usage, and limit +acp compute status + +# Top up the compute account (USDC, min 1, max 1000) +acp compute top-up --amount 50 +``` + ### Browsing Agents ```bash @@ -612,6 +624,7 @@ src/ chain.ts Chain info (list supported chains) email.ts Agent email (identity, inbox, compose, search, threads, attachments) card.ts Agent virtual cards (signup, profile, payment-method, limit, issue, 3ds) + compute.ts Agent compute account (status, top-up) lib/ config.ts Load/save config.json at ~/.config/acp/ (override with ACP_CONFIG_DIR) activeAgent.ts Active-agent resolution helpers diff --git a/SKILL.md b/SKILL.md index b98bd1f..5060cb0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -28,7 +28,7 @@ acp agent create # creates the agent identity + EVM wallet `acp configure` **opens a browser and needs an interactive human session** — it won't work for fully headless agents. Run it once on a workstation; the saved token is reusable. -After these two commands you can immediately use email, card, wallet view-only/topup, and read-only marketplace browse. Anything that signs on-chain (wallet sign/send, tokenization, marketplace job actions) additionally needs `acp agent add-signer` — covered in the recipe that needs it. +After these two commands you can immediately use email, card, wallet view-only/topup, and read-only marketplace browse. Anything that signs on-chain (wallet sign/send, tokenization, compute top-up, marketplace job actions) additionally needs `acp agent add-signer` — covered in the recipe that needs it. `ACP_CONFIG_DIR` overrides where `acp configure` saves config (default `~/.config/acp`). Other environment knobs (`IS_TESTNET`, `PARTNER_ID`) are in [Reference](#environment-variables). @@ -101,6 +101,17 @@ Auto-provisioned with the agent. View-only and on-ramp topup work immediately. S > > `sign-message` / `sign-typed-data` are not affected (they don't broadcast). Tokenization and marketplace job actions also need a signer; see [Marketplace flows](#marketplace-flows) for the latter. +### Compute + +Pay for the agent's own LLM-inference workloads from a USDC-funded compute account. `top-up` signs an on-chain USDC transfer, so it needs `acp agent add-signer` and a USDC balance in the agent's wallet on the chosen chain (`acp wallet topup` to fund it). + +| Command | What it does | Response shape | +|---|---|---| +| `acp compute status --json` | Show the compute account balance, usage, and limit | `{limit, limitRemaining, usage, ...}` | +| `acp compute top-up --amount [--chain-id ] --json` | Transfer USDC (+ a processing fee) from the agent's wallet to the ACP fee wallet to credit the compute account | `{amount, totalAmount, chainId, feeWallet, txnHash}` | + +The credited balance updates shortly after the transfer confirms — re-probe with `compute status`. + ### Marketplace (buy or sell) Hire another agent, or sell services as a provider. Backed by on-chain USDC escrow. The full flow lives in [Marketplace flows](#marketplace-flows) below — too structured to fit inline. @@ -451,6 +462,7 @@ src/ chain.ts Chain info email.ts Agent email card.ts Agent virtual cards + compute.ts Agent compute account (status, top-up) lib/ config.ts Load/save config.json at ~/.config/acp/ (override with ACP_CONFIG_DIR) activeAgent.ts Active-agent resolution diff --git a/bin/acp.ts b/bin/acp.ts index b0eae0f..686968b 100755 --- a/bin/acp.ts +++ b/bin/acp.ts @@ -17,6 +17,7 @@ import { registerSubscriptionCommands } from "../src/commands/subscription"; import { registerChainCommands } from "../src/commands/chain"; import { registerEmailCommands } from "../src/commands/email"; import { registerCardCommands } from "../src/commands/card"; +import { registerComputeCommands } from "../src/commands/compute"; const require = createRequire(import.meta.url); @@ -57,5 +58,6 @@ registerSubscriptionCommands(program); registerChainCommands(program); registerEmailCommands(program); registerCardCommands(program); +registerComputeCommands(program); program.parse(); diff --git a/src/commands/compute.ts b/src/commands/compute.ts new file mode 100644 index 0000000..306f7d6 --- /dev/null +++ b/src/commands/compute.ts @@ -0,0 +1,163 @@ +import type { Command } from "commander"; +import { encodeFunctionData, erc20Abi, isAddress, parseUnits } from "viem"; +import { USDC_ADDRESSES, USDC_DECIMALS } from "@virtuals-protocol/acp-node-v2"; +import { isJson, outputResult, outputError, isTTY } from "../lib/output"; +import { c } from "../lib/color"; +import { getClient } from "../lib/api/client"; +import { printTable } from "../lib/prompt"; +import { getActiveAgentId } from "../lib/activeAgent"; +import { createProviderAdapter, getWalletAddress } from "../lib/agentFactory"; +import { formatChainId, formatChainIds } from "../lib/chains"; +import { CliError } from "../lib/errors"; + +// ── Registration ──────────────────────────────────────────────────── + +export function registerComputeCommands(program: Command): void { + const compute = program + .command("compute") + .description("Manage agent compute (LLM-inference) accounts"); + + compute + .command("status") + .description("Show the agent's compute account balance and settings") + .action(async (_opts, cmd) => { + const { agentApi } = await getClient(); + const json = isJson(cmd); + const agentId = getActiveAgentId(json); + if (!agentId) return; + + try { + const account = await agentApi.getComputeAccount(agentId); + + if (json) { + outputResult(json, account as unknown as Record); + return; + } + + const rows: [string, string][] = [ + ["Limit", `$${Number(account.limit).toFixed(2)}`], + ["Remaining", `$${Number(account.limitRemaining).toFixed(2)}`], + ["Usage", `$${Number(account.usage).toFixed(2)}`], + ]; + printTable(rows); + } catch (err) { + outputError(json, err instanceof Error ? err : String(err)); + } + }); + + compute + .command("top-up") + .description( + "Top up the compute account by transferring USDC to the ACP fee wallet" + ) + .requiredOption("--amount ", "Amount of USDC to top up (min 1)") + .option( + "--chain-id ", + "Chain to send USDC on (defaults to the account's preferred billing chain)", + "8453" + ) + .action(async (opts, cmd) => { + const json = isJson(cmd); + try { + const amount = Number(opts.amount); + if (!Number.isFinite(amount) || amount < 1 || amount > 1000) { + throw new CliError( + `Invalid --amount: ${opts.amount}`, + "VALIDATION_ERROR", + "Top up amount must be between 1 to 1000" + ); + } + + const { agentApi } = await getClient(); + const agentId = getActiveAgentId(json); + if (!agentId) return; + + const chainId = Number(opts.chainId); + const usdcAddress = USDC_ADDRESSES[chainId]; + const usdcDecimals = USDC_DECIMALS[chainId]; + if (!usdcAddress || usdcDecimals === undefined) { + throw new CliError( + `USDC is not configured for chain ${chainId}.`, + "VALIDATION_ERROR", + `Supported chains: ${formatChainIds( + Object.keys(USDC_ADDRESSES).map(Number) + )}` + ); + } + + const provider = await createProviderAdapter(); + const supportedChainIds = await provider.getSupportedChainIds(); + if (!supportedChainIds.includes(chainId)) { + throw new CliError( + `Unsupported chain ID: ${formatChainId(chainId)}`, + "VALIDATION_ERROR", + `Supported chains: ${formatChainIds(supportedChainIds)}` + ); + } + + const { walletAddress: feeWallet, feeBps } = + await agentApi.getComputeFeeData(); + + const feeAmount = (amount * feeBps) / 10_000; + const totalAmount = amount + feeAmount; + + const value = parseUnits( + totalAmount.toFixed(usdcDecimals), + usdcDecimals + ); + + const data = encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [feeWallet as `0x${string}`, value], + }); + + if (!json && isTTY()) { + console.log( + ` Transferring ${c.bold(`${totalAmount} USDC`)} to wallet ${c.dim( + feeWallet + )} on chain ${chainId}...` + ); + } + + const txnHash = await provider.sendTransaction(chainId, { + to: usdcAddress as `0x${string}`, + data, + }); + + const agentAddress = getWalletAddress(); + const result = await agentApi.computeTopUp( + agentId, + agentAddress, + amount, + txnHash + ); + + if (json) { + outputResult(json, { + amount, + totalAmount, + chainId, + feeWallet, + txnHash, + }); + return; + } + + console.log( + `\n${c.green( + "Successfully top up your compute account, your balance will be increased shortly." + )}` + ); + printTable([ + ["Top Up Amount", `${amount} USDC`], + ["Total Paid", `${totalAmount} USDC`], + ["Chain", String(chainId)], + ["Fee Wallet", feeWallet], + ["Tx Hash", txnHash], + ]); + } catch (err) { + outputError(json, err instanceof Error ? err : String(err)); + } + }); +} diff --git a/src/lib/api/agent.ts b/src/lib/api/agent.ts index f8a022e..0b92594 100644 --- a/src/lib/api/agent.ts +++ b/src/lib/api/agent.ts @@ -462,6 +462,32 @@ export interface ThreeDSCodesResponse { codes: ThreeDSCode[]; } +// ── Compute types ─────────────────────────────────────────────────── + +export interface ComputeAccount { + limit: number; + limitRemaining: number; + usage: number; + preferredModel: string; + autoTopUpThreshold: number; + autoTopUpEnabled: boolean; + autoTopUpAmount: number; + fallbackModel: string | null; + fallbackModelThreshold: number; + hasComputeAutoBilling?: boolean; + preferredBillingChainId?: number; +} + +export interface ComputeTopUpResponse { + data?: ComputeAccount | Record; + message?: string; +} + +export interface ComputeFeeResponse { + walletAddress: string; + feeBps: number; +} + export interface TokenizeResponse { id: number; name: string; @@ -995,6 +1021,34 @@ export class AgentApi { ); } + // ── Compute methods ───────────────────────────────────────────── + + async getComputeAccount(agentId: string): Promise { + const res = await this.client.get<{ data: ComputeAccount }>( + `/agents/${agentId}/compute/account` + ); + return res.data; + } + + async getComputeFeeData(): Promise { + const res = await this.client.get<{ + data: ComputeFeeResponse; + }>(`/common/compute-fee`); + return res.data; + } + + async computeTopUp( + agentId: string, + agentAddress: string, + amount: number, + txnHash: string + ): Promise { + return this.client.post( + `/agents/${agentId}/compute/top-up`, + { amount, txnHash } + ); + } + async getAgentAssets( agentId: string, networks: string[]