Skip to content

feat(trade): route Treasures tokenized stock buys/sells through acp trade#30

Merged
psmiratisu merged 2 commits into
feat/acp-tradefrom
feat/acp-trade-treasures
Jun 3, 2026
Merged

feat(trade): route Treasures tokenized stock buys/sells through acp trade#30
psmiratisu merged 2 commits into
feat/acp-tradefrom
feat/acp-trade-treasures

Conversation

@ai-virtual-b
Copy link
Copy Markdown
Contributor

@ai-virtual-b ai-virtual-b commented Jun 3, 2026

Summary

Stacked on top of #26. Adds a Treasures Finance route to the unified acp trade command — --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; bare fetch in src/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

You pass… Route
--ticker AAPL --amount-usdc 50 Treasures buy
--ticker AAPL --amount-shares 0.1 Treasures sell
--ticker + neither / both amounts hard error with recovery hint

--ticker wins over every other path; nothing else carries stock tickers. --side stays perp-only.

Optional filters mirror the API: --protocol ondo|xstocks, --chain sol|eth, --slippage-bps <n> (default 300).

Signing — entirely behind the scenes

  1. Canonical challenge treasures-finance-quote-v1\n{issued_at}\n{sol|""}\n{eth|""} (eth lowercased; empty string for the absent side — schema is .strict())
  2. provider.signMessage(8453, challenge) for the EIP-191 eth_signature
  3. For each returned EVM leg: provider.signTypedData(typed_data.domain.chainId, typed_data) — honoring whatever chainId the server returns, not hard-coding mainnet
  4. POST /trade/submit (idempotent on (quote_id, quote_index) so a retry won't double-broadcast)
  5. Poll /quote/{id}/status (no auth) until terminal or 120 s timeout

Same keystore plumbing as #26's runSwap / HL signing — createProviderAdapter(), getWalletAddress(). Private keys never leave the keystore.

Out of scope, intentionally

  • Solana legs. Keystore is EVM-only; a solana_versioned_tx payload throws with a recovery hint pointing users at --chain eth. When Ed25519 lands, swap the throw for the real impl and --chain sol lights up.
  • Bridging Base → mainnet USDC. The existing acp trade --token-in usdc --chain-in 8453 --chain-out 1 swap 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).
  • Defaults: IS_TESTNET=truehttps://staging-api.treasures.io/public/v1, else https://api.treasures.io/public/v1.
  • Deliberately separate from ACP_SERVER_URLIS_TESTNET for 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 --noEmit clean
  • acp trade --help shows the new flags and routes (manual)
  • Direction validation: rejects neither / both amount fields with a clear recovery hint
  • End-to-end against staging-api.treasures.io: request reaches compliance layer; challenge + signature + body all accepted by every server check up to the geo-fence (which currently returns 451 unavailable_for_legal_reasons — same response from a raw curl, so not a CLI issue)
  • Full fill (completed aggregate status) — needs staging IP allowlisted, or run from an allowed region
  • Sell path on a wallet with stock-token holdings — same code path, not yet exercised against live holdings

Risk / 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 trade via a new treasures-stock intent: --ticker takes priority over swap/HL routes; --amount-usdc vs --amount-shares selects 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-completed sets exit code 1). Solana legs are rejected explicitly; EVM-only is supported today.

New src/lib/treasures/client.ts wraps the public API with HTTPS-only fetch, TREASURES_API_URL / IS_TESTNET host 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.

…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>
Comment thread src/commands/trade.ts
Comment thread src/commands/trade.ts
Comment thread src/commands/trade.ts
…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
@psmiratisu psmiratisu merged commit 2de6773 into feat/acp-trade Jun 3, 2026
1 check passed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 1 potential issue.

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 0622254. Configure here.

Comment thread src/commands/trade.ts
"`acp trade --ticker AAPL --amount-shares 0.1` (sell)."
);
}
const isBuy = amountUsdc !== undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0622254. 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.

2 participants