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>
…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
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.


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.