Skip to content

feat: acp trade (swap + Hyperliquid) + sync dev to main, with copy polish#35

Open
psmiratisu wants to merge 115 commits into
devfrom
claude/competent-raman-aac988
Open

feat: acp trade (swap + Hyperliquid) + sync dev to main, with copy polish#35
psmiratisu wants to merge 115 commits into
devfrom
claude/competent-raman-aac988

Conversation

@psmiratisu

@psmiratisu psmiratisu commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Brings dev current with main and lands the unified acp trade command (swaps + Hyperliquid deposit/spot/perp/withdraw, chain aliases, Treasures tokenized stocks, --dry-run), plus the agent-facing copy polish.

dev was ~91 commits behind main, so this PR is large by design — it syncs everything main already has, merges feat/trade-command, and adds the documentation/UX edits on top. Companion main PR: #34.

Copy changes (on top of the feature)

  • acp trade statusacp trade hl-status — renamed and clarified as the Hyperliquid account view only (HL perp positions, margin, HL spot balances); on-chain token balances come from acp wallet balance.
  • add-signer policy — help/docs now urge an explicit --policy over the restricted fallback (changing it later is manual); unrestricted documented as the choice for transacting outside Virtuals-approved contracts. Applied to agent add-signer and agent create --signer.
  • Spot copy trimmed — removed concrete HL-spot how-to (examples, USDC-quoted note, table row); kept spot as a conceptual venue.
  • Trimmed backend-plumbing prose to observable behavior.

🤖 Generated with Claude Code


Note

High Risk
Introduces automated signing and broadcasting for swaps, bridges, Hyperliquid, and Treasures stock flows via a new backend-driven trade loop—high financial and key-handling exposure despite tests covering only the sign branch.

Overview
This PR syncs dev with main and ships the package as @virtuals-protocol/acp-cli (bundled dist/bin/acp.js, Node ≥20.19, version 1.0.14), with a much broader CLI surface and agent-oriented docs.

The headline feature is unified acp trade: one entry point where chain IDs route intent (EVM↔EVM swaps, deposit to Hyperliquid via 1337, HL spot, perps via --side, withdraw-from-hl, plus Treasures tokenized stock flows). The CLI calls /trade/plan + /trade/next, signs send txs and new sign steps (EIP-712 / personal proof), supports --dry-run, and exposes acp trade hl-status (renamed from status) as the HL-only account read. scripts/tradeLoopSign.test.ts exercises the sign branch.

Beyond trading, bin/acp.ts wires subscription, email, card, compute, skill, and trade commands; ACP_CONFIG_DIR overrides config location; configure start / configure complete support non-blocking OAuth for harnesses; headless partner bootstrap (generate-signer-key, link, token flags) is documented in docs/headless-deployment.md.

README and SKILL.md are rewritten around identity vs marketplace, wallet/email/card/compute, subscriptions, reviews, job watch, and strict “agent runs CLI, human only clicks URLs” guidance (including acp skill check for stale skills). Copy polish: explicit add-signer --policy, and docs that hl-status is not on-chain wallet balance.

Reviewed by Cursor Bugbot for commit 49a9b65. Bugbot is set up for automated code reviews on this repo. Configure here.

andrew-virtuals and others added 30 commits April 9, 2026 22:59
refactor: remove job offering isPrivate field
feat: add update agent details command
feat: enhance token management by adding wallet address support for t…
…ion steps and legacy agent search instructions
* feat: update tokenization process and documentation

* fix: update tokenization docs

* fix: ensure transaction receipt handling in tokenization functions

* fix: improve error handling for reverted transactions in receipt processing

* refactor: remove unused ABI files and streamline tokenization functions

* docs: enhance tokenization documentation

* docs: clarify and expand tokenization options in SKILL and tokenization documentation

---------

Co-authored-by: Zuhwa <zuhwa@virtuals.io>
feat: erc8004 identity & reputation
Wire up the new agent-email and agentcard APIs from agentic-commerce-be:

  - email: whoami, provision, inbox, compose, search, thread, reply,
    extract-otp, extract-links
  - card: signup, signup-poll, whoami, profile (get/set/reset),
    payment-method, limit (get/set), issue, list, get

Card uses the spend-request model. The BE returns a `nextStep` breadcrumb
on every mutating response; the CLI surfaces it so agents can walk the
setup flow (profile -> payment method -> limit -> issue) without
introspecting profile fields.

Also adds PATCH support to ApiClient for profile updates.
  acp email attachment --attachment-id <id> [--output <dir>]

Two-step download wired to the new BE routes:

  GET /agents/:id/email/attachments/:id           → metadata
  GET /agents/:id/email/attachments/:id/download  → binary stream

Streams bytes straight to disk with stream.pipeline (back-pressure
respecting) instead of buffering, so large attachments don't sit in
memory. Filename comes from the upstream Content-Disposition header
(RFC 5987 aware), falling back to the metadata filename if missing.
path.basename() strips any separator upstream might inject, so a
malicious header can't traverse the caller's filesystem.

Requires agentic-commerce-be#94.
card.ts and email.ts both defined an identical formatDate helper.
Moving to lib/output.ts removes the duplication and keeps a single
place to evolve ISO-timestamp rendering (locale, formatting).

Addresses bugbot review on #8.
`new Date(bad)` doesn't throw — it returns an Invalid Date object,
and `.toLocaleString()` on that returns the literal string "Invalid
Date". The try/catch fallback was dead code, so callers with bad
server timestamps saw "Invalid Date" in tables instead of the raw
ISO string the comment promised.

Switch to an `isNaN(d.getTime())` check, which is the standard way
to detect an Invalid Date, and return the raw string when it fires.

Addresses bugbot review on #8.
The `whoami` handler formatted `identity.createdAt` with raw
`new Date(iso).toLocaleString()` instead of the `formatDate` helper
it already imports — so a malformed server timestamp would render
as "Invalid Date" in the table rather than falling back to the raw
ISO string.

Addresses bugbot review on #8.
psmiratisu and others added 29 commits June 4, 2026 18:15
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…trade` (#30)

Adds a Treasures Finance route to `acp trade` — `--ticker` triggers a new
"treasures-stock" intent and runs ownership-proof → quote → per-leg EIP-712
sign → submit → poll-status in one command. New `src/lib/treasures/client.ts`
wraps the public Treasures API over `fetch` with HTTPS enforcement and
TREASURES_API_URL / IS_TESTNET host selection.

Also hardens the flow per review:
- poll status once more after the final sleep so a late fill isn't misreported
  as TIMEOUT
- non-zero exit code on terminal partial_failed / all_failed
- validate --slippage-bps as a non-negative integer instead of sending null
A stock symbol is now named with --token everywhere; the companion flag
picks the venue: --side -> HL perp, --amount-usdc/--amount-shares -> Treasures
tokenized stock (spot). Drops the separate --ticker flag so an agent picks the
asset once and the mode second, matching the CLI's params-decide-the-venue model.

https://claude.ai/code/session_01C56LaYyFW7iRxtdY5tY3uA
Move the Treasures tokenized-stock flow out of the CLI and into the
trading-agent planner. The CLI is now a thin signer: it POSTs a plan with
a `treasures` block and drives the same /trade/plan + /trade/next loop as a
swap, signing whatever the server asks via the new `sign` action.

- runTradeLoop: handle `sign` actions (signTypedData / signMessage → post
  signature back).
- runTreasuresStock: build a plan + run the loop; delete the ~150 lines of
  direct quote/sign/submit/poll and the now-dead lib/treasures/client.ts.
- detectIntent: route a funded buy (--token ticker + --token-in/--chain-in/
  --amount-in, no --token-out/--chain-out) to Treasures, so the server can
  bridge any chain's funds to USDC@eth before buying.
- scripts/: tsx harnesses for the sign branch + detectIntent routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebasing the trade feature onto main surfaced two integration fixes:
- package-lock.json: regenerate to include @nktkas/hyperliquid (the lock
  was taken from main during conflict resolution and lacked it).
- getAgentApi: drop the walletAddress arg — main simplified getClient to
  (unauthenticated?: boolean), so passing a string no longer typechecks.
  The param was always ignored (resolveToken uses the config owner wallet).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add chain ID legend to --help (1 ETH, 42161 ARB, 8453 Base, 1337 HL)
- Lead command description with cross-chain support
- Clarify --chain-in/--chain-out flag descriptions with named chains
- Clarify --amount-in is spend amount, not receive amount
- Add --size/--leverage note in perp routing table
- ensureHlFunds: actionable error with deposit command + leverage tip
- --size error: explicitly say 'token units, not USD', show --leverage example
- Progress labels: human-readable (Opening long / Buying / Selling vs Market long/buy/sell)
- Spot validation: plain English instead of internal jargon
- chains: parseChainArg() accepts named chain aliases (hyperliquid/hl, base,
  arb, eth, optimism, …) alongside numeric ids, case-insensitive. trade
  normalizes --chain-in/--chain-out up front so intent routing and the backend
  both get a clean id — `--chain-out hyperliquid` now works.
- trade: new --dry-run flag previews any trade without signing/submitting.
  Swap/deposit/Treasures send dryRun to /trade/plan and render the server's new
  `preview` action; HL perp/spot/withdraw (signed client-side) compute and print
  their preview locally — size, notional, and margin at the effective leverage —
  without setting leverage, moving funds, or placing the order.
- help: clarify --size is in token units (not USD); document aliases + --dry-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes two "two-ways-to-do-one-thing" footguns flagged in review:

- Withdraw: collapse to one command. `acp trade withdraw-from-hl --amount <n>`
  is now the ONLY way to move USDC off Hyperliquid. Dropped the duplicate
  `--chain-in 1337 --chain-out <evm>` intent route and the ceremony it required
  (an explicit --chain-out whose only legal value was 42161 — HL's withdraw3
  always settles to Arbitrum, so it was never a chain choice). --chain-in 1337
  now means spot only; anything else points to the command. Adds --dry-run.

- Slippage: one flag instead of two. `--slippage <pct>` (percent) now covers
  every route; removed `--slippage-bps` (the duplicate with a different unit
  that made `--slippage 300` mean wildly different things per route). Swaps/
  Treasures convert pct→bps via slippageBpsFromPct; HL already used percent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…thdraw

Backward-compat: existing callers/agents that move USDC off Hyperliquid via the
chain-in/chain-out combo shouldn't break now that `withdraw-from-hl` is the
canonical command. detectIntent routes `1337 → 42161` to the same withdrawal.

HL's withdraw3 only ever settles to Arbitrum, so we honor 42161 and reject any
other chain-out (with a message pointing at withdraw-from-hl) rather than
silently sending to a chain HL can't reach. Docs/help updated to show the combo
as an alias of withdraw-from-hl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dge onward)

- Default: a bare HL withdraw (no --chain-out / --to-chain) settles to Arbitrum,
  where HL's withdraw3 always lands.
- --chain-in 1337 --chain-out <evm> (or withdraw-from-hl --to-chain <id>) to a
  NON-Arbitrum chain now withdraws to Arbitrum first, waits for the funds to
  land (polls Arbitrum USDC balance, ~few min), then bridges the actual arrived
  amount onward via the normal swap/LiFi route. On settle-timeout it returns the
  exact follow-up bridge command — funds are never stranded without a next step.
- detectIntent: chain-in 1337 with a non-USDC token-out still errors as a spot
  order missing --chain-out 1337, so a typo can't silently move funds off HL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`acp trade` with no flags previously dropped into a readline wizard. Drop it:
the CLI is agent-first, and the wizard was a parallel input surface that could
drift from the flag flows. With no intent in the flags, the command now errors
with a pointer to `--help` instead. Removes runInteractive, the "interactive"
intent, and the now-unused readline/prompt imports (the prompt lib stays — other
commands use it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CLI is now a pure signer for Hyperliquid too. runPerp / runHlSpot /
runWithdraw no longer touch the @nktkas ExchangeClient — they POST hl / hlSpot /
hlWithdraw to /trade/plan and drive the existing runTradeLoop sign loop (the
server builds each EIP-712 action; the CLI signs with the agent wallet).

- Deletes the client-side execution: order placement, leverage, spot↔perp
  auto-balance (ensureHlFunds), the client withdraw-then-bridge, and the
  exitAfterOrder/placeHlOrder SDK-quirk workarounds.
- lib/hl/client.ts shrinks to a read-only InfoClient for `trade status`; the
  ExchangeClient/signer wiring and order/price helpers are gone.
- --dry-run, chain aliases, and the unified --slippage flag all keep working,
  now fully server-driven (the backend returns a `preview` action).

Net −495 lines. `trade status` stays a client-side read (no signing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s intent

The CLI no longer routes. `acp trade` collects whatever flags were passed into a
flat body and POSTs to /trade/plan; the backend detects the venue and drives the
sign loop. Deletes detectIntent + all per-venue run* functions (runSwap /
runHlSpot / runPerp / runWithdraw / runTreasuresStock) and their helpers
(isUsdcSymbol, validateTreasures*, parseSlippage, slippageBpsFromPct,
parseChainArg usage). `withdraw-from-hl` stays as a thin alias that forwards the
equivalent flat request; `status` stays a read-only HL query.

Net big deletion; CLI is now a pure flag-forwarder + signer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`acp trade status` calls the new POST /trade/hl-status instead of reading HL
directly. Deletes src/lib/hl/client.ts (the last HL SDK usage) and removes
@nktkas/hyperliquid from package.json. The CLI now has zero Hyperliquid deps —
all HL access (trade + status) goes through the backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
detectIntent was deleted (intent detection moved to the backend), so its harness
no longer applies. tradeLoopSign.test.ts stays — runTradeLoop is still the CLI's
sign loop and it passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sync package-lock.json after dropping the HL dependency, so `npm ci` no longer
installs @nktkas/hyperliquid (or its transitive valibot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…policy, trim spot copy

- Rename `acp trade status` to `acp trade hl-status`; clarify it's HL-account-only
  and that on-chain balances come from `acp wallet balance`.
- add-signer: make the help/docs urge an explicit --policy (fallback is restricted,
  changing it later is manual); note unrestricted is for non-Virtuals contracts.
- Remove concrete HL-spot how-to (examples + USDC-quoted note + table row); keep
  spot as a conceptual venue. Trim backend-plumbing prose to observable behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 49a9b65. Configure here.

Comment thread src/commands/trade.ts
`Trade ${plan.tradeId.slice(0, 8)}` +
(plan.direction && plan.route ? ` (${plan.direction} via ${plan.route})` : "")
);
const result = await runTradeLoop(apiUrl, token, provider, plan, json);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dry-run still requires signer

Medium Severity

runTrade always calls createProviderAdapter() before /trade/plan, so --dry-run fails with NO_SIGNER even when the plan would only return a preview and never sign. Help text says dry-run previews without submitting anything.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 49a9b65. Configure here.

Comment thread src/commands/trade.ts
} catch (err) {
outputError(json, err instanceof Error ? err : String(err));
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing trade interactive picker

Medium Severity

README and SKILL state that bare acp trade in a TTY opens an interactive picker, but the command action always calls runTrade with parsed flags only. With no flags, the CLI posts a minimal body to /trade/plan instead of prompting.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 49a9b65. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants