Uh oh!
There was an error while loading. Please reload this page.
feat(trade): route Treasures tokenized stock buys/sells through acp trade - #30
Conversation
…trade` Adds a Treasures Finance route to the unified `acp trade` command from #26: `--ticker <SYM>` triggers a new "treasures-stock" intent that wins over swap/HL/perp routing (no other path carries stock tickers). Direction comes from the amount field — `--amount-usdc` for buy, `--amount-shares` for sell — keeping `--side` perp-only. End-to-end inside one command, signing internal: 1. build canonical challenge (`treasures-finance-quote-v1\\n{ts}\\n{sol|""}\\n{eth|""}`) 2. `provider.signMessage(8453, challenge)` — EIP-191 ownership proof 3. POST `/quote/buy` or `/quote/sell` 4. `provider.signTypedData(domain.chainId, typed_data)` per EVM leg 5. POST `/trade/submit` 6. poll `/quote/{id}/status` until terminal `src/lib/treasures/client.ts` is bare `fetch` — no SDK, one import (`CliError`). HTTPS enforced, matching the trade.ts proxy's posture. `TREASURES_API_URL` overrides the default host (defaults to staging on `IS_TESTNET=true`, prod otherwise — decoupled from ACP_SERVER_URL so tests can mix prod-ACP + staging-Treasures). Solana legs are deliberately unsupported in this first cut: the keystore is EVM-only, so a `solana_versioned_tx` payload throws with a recovery hint instead of silently dropping the leg (which would 400 `incomplete_submit` on the server anyway). `--chain eth` filters them out at quote time. Bridging Base → mainnet USDC is also intentionally out of scope here — the existing `acp trade --token-in usdc --chain-in 8453 --chain-out 1` swap path already does it via `/trade/plan`. Tested end-to-end against staging-api.treasures.io up to the HTTP layer: signature accepted, schema accepted, request reached the compliance layer. Staging is currently 451 geo-fenced (same response from a raw curl, so not a CLI issue) — needs a whitelist for full fill-status verification. `tsc --noEmit` clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…validation - pollTreasuresStatus: re-check status after the final sleep so a fill that lands during that window is observed instead of misreported as TIMEOUT - runTreasuresStock: set a non-zero exit code on terminal partial_failed/ all_failed so callers can branch on failure - validate --slippage-bps as a non-negative integer rather than letting a NaN serialize to max_slippage_bps: null
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0622254. Configure here.
| "`acp trade --ticker AAPL --amount-shares 0.1` (sell)." | ||
| ); | ||
| } | ||
| const isBuy = amountUsdc !== undefined; |
There was a problem hiding this comment.
Empty Treasures amount accepted
Medium Severity
Treasures buy/sell direction only checks whether --amount-usdc or --amount-shares is present, not whether the value is non-empty or a valid positive amount. An empty string still counts as “set,” so the flow can sign the ownership proof and call /quote/buy or /quote/sell with blank amounts instead of the intended exactly-one-amount validation.
Reviewed by Cursor Bugbot for commit 0622254. Configure here.
* feat: acp trade — unified swap / Hyperliquid command
A single `acp trade` command for token swaps (same-chain and cross-chain via
the trading-agent server's BondingV5 / LiFi routing) and Hyperliquid (deposit,
spot, perp, withdraw). Routes by the params you pass: Hyperliquid is chain 1337,
so the chains decide the venue; `--side long|short` is a perp. Auto-balances the
HL perp/spot wallets so `deposit → trade` just works. Signing stays client-side
via the keystore signer; HL orders are EIP-712 actions.
Adds @nktkas/hyperliquid and bumps @virtuals-protocol/acp-node-v2 to ^0.1.2
(required for the provider signTypedData / sendTransaction APIs the trade flow
uses). Requires Node >=20.19 (engines) for the HL SDK. Documents the command in
README.md and SKILL.md.
Addresses review:
- spot market orders look up mids by `@{pairIndex}` (PURR special-cased), not
the pair name, which returned no mid.
- detectIntent gates perp on --side long|short so a stray --token can't
pre-empt chain-based spot routing.
- declare engines.node >=20.19.0 to match @nktkas/hyperliquid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: note HL perps cover stocks, FX & commodities on leverage
Hyperliquid lists leveraged perp markets beyond crypto (equities,
currencies, commodities); the acp trade --side flags are identical
across asset classes. Surface this in the SKILL.md frontmatter +
trading section and the README perps section so agents know they
can open stock/FX/commodity positions, not just crypto.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(trade): note perp asset classes in --token help and --help text
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(trade): route Treasures tokenized stock buys/sells through `acp 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
* refactor(trade): unify stock asset flag on --token (drop --ticker)
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
* feat(trade): route Treasures stock trades through the planner
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>
* fix: integrate acp trade onto latest main
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>
* fix(trade): improve UX copy and error messages
- 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
* feat(trade): chain aliases + --dry-run preview across all routes
- 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>
* refactor(trade): single withdraw path + one slippage flag
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>
* feat(trade): keep `--chain-in 1337 --chain-out 42161` working as a withdraw
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>
* feat(trade): withdraw from HL to any chain (Arbitrum direct, else bridge 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>
* refactor(trade): remove the interactive picker
`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>
* refactor(trade): route all HL trading through the backend planner
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>
* refactor(trade): one `acp trade` — forward flat flags, backend detects 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>
* refactor(trade): drop @nktkas/hyperliquid — status now via backend
`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>
* chore(trade): remove obsolete detectIntent.test.ts
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>
* chore: prune @nktkas/hyperliquid + valibot from the lockfile
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>
* docs(trade): rename `trade status`→`hl-status`, push explicit signer 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>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: ai-virtual-b <bryan@virtuals.io>
Co-authored-by: Agent <agent@virtuals.io>

Summary
Stacked on top of #26. Adds a Treasures Finance route to the unified
acp tradecommand —--ticker <SYM>triggers a new"treasures-stock"intent and runs ownership-proof → quote → per-leg EIP-712 sign → submit → poll-status in one command. No SDK; barefetchinsrc/lib/treasures/client.ts(one import:CliError).Why it slots into
acp trade(and not its own command)Treasures buy/sell settles an ERC-20 stock token into the wallet (you own a token, not a position) — topologically identical to a swap. Sitting next to BondingV5/LiFi swaps and HL spot/perp keeps the param-routing pattern from #26 intact.
Intent routing
--ticker AAPL --amount-usdc 50--ticker AAPL --amount-shares 0.1--ticker+ neither / both amounts--tickerwins over every other path; nothing else carries stock tickers.--sidestays perp-only.Optional filters mirror the API:
--protocol ondo|xstocks,--chain sol|eth,--slippage-bps <n>(default 300).Signing — entirely behind the scenes
treasures-finance-quote-v1\n{issued_at}\n{sol|""}\n{eth|""}(eth lowercased; empty string for the absent side — schema is.strict())provider.signMessage(8453, challenge)for the EIP-191eth_signatureprovider.signTypedData(typed_data.domain.chainId, typed_data)— honoring whatever chainId the server returns, not hard-coding mainnet/trade/submit(idempotent on(quote_id, quote_index)so a retry won't double-broadcast)/quote/{id}/status(no auth) until terminal or 120 s timeoutSame keystore plumbing as #26's
runSwap/ HL signing —createProviderAdapter(),getWalletAddress(). Private keys never leave the keystore.Out of scope, intentionally
solana_versioned_txpayload throws with a recovery hint pointing users at--chain eth. When Ed25519 lands, swap thethrowfor the real impl and--chain sollights up.acp trade --token-in usdc --chain-in 8453 --chain-out 1swap path already handles it via/trade/plan. No reason to mirror it in a Treasures-specific command.Config
TREASURES_API_URL— overrides the host (lets ops point at non-prod without rebuilding).IS_TESTNET=true→https://staging-api.treasures.io/public/v1, elsehttps://api.treasures.io/public/v1.ACP_SERVER_URL—IS_TESTNETfor ACP and Treasures are independent dials. Mixing prod-ACP + staging-Treasures is the realistic test posture (an agent's wallet/signer live on prod ACP regardless of which Treasures env you're hitting).Test plan
tsc --noEmitcleanacp trade --helpshows the new flags and routes (manual)staging-api.treasures.io: request reaches compliance layer; challenge + signature + body all accepted by every server check up to the geo-fence (which currently returns451 unavailable_for_legal_reasons— same response from a raw curl, so not a CLI issue)completedaggregate status) — needs staging IP allowlisted, or run from an allowed regionRisk / blast radius
Medium. Auto-signs a real EIP-712 Fusion order and POSTs it to a public production-ish API when staging compliance is off. Mitigations: HTTPS-enforced like the existing trade proxy; idempotent submit; 120 s poll timeout that surfaces a TIMEOUT rather than hanging; Sol legs throw rather than half-submit.
🤖 Generated with Claude Code
Note
Medium Risk
The CLI auto-signs real EIP-712 Fusion orders and submits them to Treasures production/staging APIs; mitigations include HTTPS enforcement, idempotent submit, Sol-leg refusal, and poll timeout, but failed/partial fills still move real funds on EVM.
Overview
Adds Treasures Finance tokenized stock trading to
acp tradevia a newtreasures-stockintent:--tickertakes priority over swap/HL routes;--amount-usdcvs--amount-sharesselects buy vs sell, with optional--protocol,--chain, and--slippage-bps.The flow signs an ownership-proof challenge, fetches a quote, signs each returned EIP-712 leg with the keystore, submits to Treasures, and polls until a terminal status (non-
completedsets exit code 1). Solana legs are rejected explicitly; EVM-only is supported today.New
src/lib/treasures/client.tswraps the public API with HTTPS-onlyfetch,TREASURES_API_URL/IS_TESTNEThost selection, and typed quote/submit/status helpers.Reviewed by Cursor Bugbot for commit 0622254. Bugbot is set up for automated code reviews on this repo. Configure here.