Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -89,7 +89,7 @@ acp <command> [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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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 <usdc 1–1000> [--chain-id <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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions bin/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -57,5 +58,6 @@ registerSubscriptionCommands(program);
registerChainCommands(program);
registerEmailCommands(program);
registerCardCommands(program);
registerComputeCommands(program);

program.parse();
163 changes: 163 additions & 0 deletions src/commands/compute.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>);
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>", "Amount of USDC to top up (min 1)")
.option(
"--chain-id <id>",
"Chain to send USDC on (defaults to the account's preferred billing chain)",
"8453"
)
Comment thread
andrew-virtuals marked this conversation as resolved.
.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));
}
});
}
54 changes: 54 additions & 0 deletions src/lib/api/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
message?: string;
}

export interface ComputeFeeResponse {
walletAddress: string;
feeBps: number;
}

export interface TokenizeResponse {
id: number;
name: string;
Expand Down Expand Up @@ -995,6 +1021,34 @@ export class AgentApi {
);
}

// ── Compute methods ─────────────────────────────────────────────

async getComputeAccount(agentId: string): Promise<ComputeAccount> {
const res = await this.client.get<{ data: ComputeAccount }>(
`/agents/${agentId}/compute/account`
);
return res.data;
}

async getComputeFeeData(): Promise<ComputeFeeResponse> {
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<ComputeTopUpResponse> {
return this.client.post<ComputeTopUpResponse>(
`/agents/${agentId}/compute/top-up`,
{ amount, txnHash }
);
Comment thread
andrew-virtuals marked this conversation as resolved.
}

async getAgentAssets(
agentId: string,
networks: string[]
Expand Down