> For the complete documentation index, see [llms.txt](https://docs.trover.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.trover.tech/engineering/agent-orchestration.md).

# Capability-driven agent orchestration

Every website message, signed X mention/reply, and X DM creates the same persisted `AgentRun`. Trover resolves authenticated website requests from the verified Privy subject and X requests from the signed X author id. A linked X identity shares a principal with its `TradingProfile`; an unlinked X identity receives an isolated, research-only principal. Anonymous website chat uses an opaque, 24-hour HttpOnly guest-principal cookie that never grants wallet authority.

`POST /v1/agent/chat` creates an `AgentRun` and immediately returns a run id. The `trover-agent` worker serializes runs with a Redis lock per principal, plans against the server-owned registry, and stores only verified results. Clients poll `GET /v1/agent/runs/:id`; that endpoint is scoped to the caller's profile or matching guest cookie, so ids cannot be used to access another principal's run.

The planner sees only the registered capabilities and tool metadata. It cannot call arbitrary HTTP, database, shell, or transaction functions. The server still validates ownership, current X linkage, wallet state, assets, routes, quotes, simulation, limits, confirmation policy, and signing capability before queuing/submitting any transaction. Reasoning is bounded to four rounds and twelve verified read-tool calls; only one idempotent action intent may be created by a run.

The OpenAI planner is authoritative whenever it returns a schema-valid plan. Deterministic parsers provide explicit literal hints, validate action fields, and provide an observable provider/schema fallback; they never replace a valid model interpretation. `AgentRun.plan` records planner success, fallback reason, parser use, conflicts, field provenance, tool results, and normalized input/output assets.

Account linkage gates private read and transaction tools, not the conversation. An unlinked user can research markets, discuss a trade, or ask how Trover works. If execution is requested, the verified runtime returns `link_account` or `select_wallet` without creating an intent.

Trade, bridge, transfer, and launch workers emit structured action results. They do not compose or publish replies. The agent response layer receives the original request, conversation, plan, tool results, action/account context, and safety constraints. Protected facts are server-owned; a model draft that drops or changes an amount, address, hash, URL, or action state is rejected. One edge publisher delivers the resulting response to web, X mentions, or X DMs.

X delivery reserves one publication slot per inbound mention/reply or DM. Transaction runs do not publish queued or submitted chatter; they wait for a terminal result. A required confirmation, wallet signature, or account link may use that slot immediately because the workflow cannot continue without the user. A later user ping creates a new run and therefore a new response slot. Provider payloads remain in structured traces; X receives a short plain-language cause and whether a transaction was submitted.

## Required runtime variables

Set the same non-empty random value in both API and worker environments:

```env
AGENT_INTERNAL_API_URL=http://api:3001
AGENT_ORCHESTRATOR_KEY=
AGENT_RUN_TIMEOUT_SECONDS=45
```

Generate the key with `openssl rand -hex 32`. It is server-only: do not place it in Vercel's `NEXT_PUBLIC_*` variables, browser code, X settings, or logs.

Draft action slots and the last 20 conversation turns are retained for 24 hours. A submitted active action remains resolvable for its full lifecycle; terminal facts are retained as `last_transaction` for 24 hours. Explicit preferences remain until the owner uses the profile settings or `DELETE /v1/agent/preferences`.

## Capability catalog (registry as of 2026-09-08)

`packages/domain/src/agent-capabilities.ts` is the only catalog the planner sees; ids live in `agentCapabilityIdSchema` in `packages/core/src/schemas.ts`. Registering a capability there updates the planner prompt, the MCP tool list and `tests/agent-planner.test.ts` in one place. Fifty-four capabilities are registered today. `read` never moves money; `transaction` requires an explicit action, a linked executable wallet and action mode, and is refused when phrased as a question.

### Conversation, wallet and chain reads

| Capability            | Risk | Data source                                                                               |
| --------------------- | ---- | ----------------------------------------------------------------------------------------- |
| `conversation`        | read | none; product, wallet-linking and memecoin talk without a tool                            |
| `wallet_balance`      | read | RPC balances of the linked canonical wallet with USD values                               |
| `portfolio`           | read | Trover portfolio service (positions, value, P\&L)                                         |
| `transaction_history` | read | verified RPC transactions, receipts and ERC-20 transfer logs                              |
| `transaction_status`  | read | RPC receipt decoded with method, transfers, gas, confirmations                            |
| `token_analysis`      | read | token identity, market, contract and risk evidence                                        |
| `contract_analysis`   | read | contract source, ABI, proxy and on-chain verification                                     |
| `token_listing`       | read | Trover supported trade-token registry                                                     |
| `stock_listing`       | read | Robinhood Stock Token registry                                                            |
| `stock_quote`         | read | verified Robinhood Stock Token quote                                                      |
| `stock_holdings`      | read | held stock tokens of the linked wallet                                                    |
| `stock_pnl`           | read | stock-token P\&L and valuation                                                            |
| `stock_claims`        | read | claim index and the distributor's claimed state                                           |
| `market_overview`     | read | live token or stock market snapshot (price, liquidity, FDV, volume, source)               |
| `market_chart`        | read | historical USD chart for a token or stock; an NFT entity is routed to `nft_price_history` |
| `market_trends`       | read | CoinGecko trends, research only                                                           |
| `fee_revenue`         | read | treasury ledger totals with unpriced-record counts                                        |
| `explain_transaction` | read | verified transaction, receipt, decoded calldata, logs, linked Trover action               |
| `address_book`        | read | the caller's private aliases and linked X recipients                                      |
| `auto_invest_status`  | read | monthly stock auto-invest plans, executions, fees, performance                            |
| `dex_order_status`    | read | the caller's limit, take-profit, stop-loss, trailing-stop and DCA orders with runs        |

### Transactions

| Capability            | Risk        | What it does                                                                      |
| --------------------- | ----------- | --------------------------------------------------------------------------------- |
| `auto_invest_manage`  | transaction | create, update, pause, resume or cancel a monthly stock auto-invest plan          |
| `dex_order_manage`    | transaction | create, pause, resume or cancel a profile-owned DEX order                         |
| `portfolio_rebalance` | transaction | convert held stock or liquid tokens into USDG, ETH, a stock or an addressed token |
| `trade`               | transaction | swap or buy/sell a supported asset or stock token                                 |
| `bridge`              | transaction | Relay EVM bridge or cross-chain swap                                              |
| `token_launch`        | transaction | Pons token deployment                                                             |
| `transfer`            | transaction | send native ETH or an addressed ERC-20 to an EVM recipient                        |
| `fee_claim`           | transaction | collect Pons Launch Locker creator fees                                           |
| `burn`                | transaction | send caller-owned ETH or an ERC-20 to the burn address                            |
| `contract_action`     | transaction | advanced EVM call with caller-supplied chain, contract, calldata, value           |

### NFT market

Since 2026-09-08 every NFT read answers from Trover's own index, not from OpenSea's REST API. The worker calls the API's NFT routes over `AGENT_INTERNAL_API_URL` (`apps/worker/src/nft-market-reads.ts`, one reader per capability) and the MCP server injects the same routes in-process (`localNftApi` in `apps/api/src/mcp-routes.ts`), so an agent states exactly what the terminal shows. OpenSea is consulted only where the table says so.

| Capability              | Risk        | Data source                                                                                                                            |
| ----------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `nft_search`            | read        | index search by name, slug or contract across supported chains; OpenSea only when the index has no match                               |
| `nft_portfolio`         | read        | the caller's OpenSea inventory                                                                                                         |
| `nft_market_snapshot`   | read        | indexed detail plus orderbook (`/collections/:slug`, `/orderbook`); OpenSea only as a fallback                                         |
| `nft_collection_detail` | read        | `/collections/:slug`: floor, 24h volume and sales, owners, listed, supply, lifecycle, mint schedule and price, last mint and last sale |
| `nft_price_history`     | read        | `/collections/:slug/candles`: sale candles and floor points for an interval and window, with min, max, last, change                    |
| `nft_activity`          | read        | `/collections/:slug/activity`: last 25 events and counts by type for a window                                                          |
| `nft_orderbook`         | read        | `/orderbook` and `/listings?sort=rank`: cheapest listings, cheapest by rank, top offers, spread, listed count                          |
| `nft_traits`            | read        | `/traits` and `/items?sort=rank`: trait counts and rarity, top-ranked tokens                                                           |
| `nft_holders`           | read        | `/holders`: unique holders and percent, top holders, top-10 concentration                                                              |
| `nft_drops`             | read        | `/trending?lifecycle=…`, `/collections?mode=new`, or one collection's mint state (minted of max, mints per hour, last mint, price)     |
| `nft_trending`          | read        | `/trending`: ranked rows for a window and lifecycle tab                                                                                |
| `nft_discovery`         | read        | `/collections?mode=trending\|top\|new`: the discovery leaderboards the worker warms every minute                                       |
| `nft_snipe_status`      | read        | Prisma `nftSnipePlan`: the caller's plans with fills, spend, remaining budget, expiry, last failure                                    |
| `nft_snipe_manage`      | transaction | arms or cancels an unattended snipe plan (rules below)                                                                                 |
| `token_economics`       | read        | `/v1/token/economics`: fee split, buyback and burn totals, holder tranches, league pool                                                |
| `nft_mint`              | transaction | mint from a live OpenSea drop                                                                                                          |
| `nft_buy`               | transaction | buy a selected listing                                                                                                                 |
| `nft_sweep`             | transaction | sweep up to fifty listings within an item count and a max price per item                                                               |
| `nft_list`              | transaction | list caller-owned NFTs                                                                                                                 |
| `nft_offer`             | transaction | make an NFT or collection offer                                                                                                        |
| `nft_accept_offer`      | transaction | accept a selected offer for a caller-owned NFT                                                                                         |
| `nft_cancel_order`      | transaction | cancel a caller-owned listing or offer                                                                                                 |
| `nft_transfer`          | transaction | transfer a caller-owned NFT to a validated EVM recipient                                                                               |

## NFT read contract

Every indexed read returns `NftReadResult` from `@trover/domain`: `{ capability, entity, units, freshness, summary, data }`. The pieces the presenter must respect:

* **Freshness.** `freshness` is built by `nftFreshnessFrom` from the live sale lane's Redis lag record for the chain: `source` (`indexed`), `chain`, `indexedBlock`, `headBlock`, `lagBlocks`, `indexedAt`, `ageSeconds`, `servedAt`. The age is computed from `indexedAt` at read time. The reply states it ("data 12s ago"); if the lane is silent the age is `null` and the summary says "age unknown". Freshness is per chain, so a Robinhood read never wears an Ink block number.
* **Summary.** `compactNftSummary` builds one deterministic line of at most 200 characters from whatever the read found, in this order: name, lifecycle (unless secondary), minted of max, floor (ETH and USD), 24h volume and sales, owners of supply, listed, last mint age, last sale age, a capability-specific `extra`, and the data age last. Parts are dropped from the end (the age stays) until the line fits. The model may write around the summary; it may not change the numbers inside it. `tests/agent-nft-reads.test.ts` holds the length and content cases.
* **Channel split.** `readReply(plan, profile, channel)` in `apps/worker/src/agent-orchestrator.ts` receives the run's channel. On `x_mention` and `x_dm` the reply is the summary alone (it fits under `MAX_REPLY_LENGTH`, 250). On the web the reply is the summary followed by the JSON `data` block, which the chat renders as the detail. The same rule applies to `nft_snipe_status`.
* **Entity resolution.** `resolveNftCollection` resolves in this order: a contract address (looked up in `nftCollectionMarket`, highest priority row wins) is unambiguous; then a slug on the requested chain (the detail route settles the chain and redirects a mis-filed slug); then a name search in the index (`/search`) accepting an exact folded name or slug match, or a single hit. Anything else replies `collection_not_found` with up to three closest candidates named as `name (chain/slug)`. The MCP server uses the same order (`resolveIndexedCollection`).
* **Failure.** A route error becomes `<capability>_unavailable: <reason>` and is logged; the agent never substitutes OpenSea numbers for an indexed read.

## Snipe management rules

`nft_snipe_manage` is the only NFT transaction that does not go through the action flow: it arms or cancels a plan directly with the same rules the terminal's route applies (`manageNftSnipe`):

1. `NFT_SNIPE_ENABLED` must be on (it defaults to true); otherwise `nft_snipe_disabled`.
2. A cancel targets `snipeId` when given, else the caller's most recent armed plan for the named collection. Fills already made stay on record.
3. Arming needs the caller's active canonical wallet, and that wallet must pass `delegatedExecutionWallet`: delegated trading on, an embedded Privy wallet (never external), `walletMode` `embedded_user_owned_delegated`, `policyProfile` `delegated_low_limit`. Otherwise `delegated_execution_wallet_required`.
4. Per-trade confirmation must be off (`confirmation_required_incompatible`), a plan that buys with no browser open cannot stop to ask.
5. The body is validated by `nftSnipePlanSchema`: `trigger` `on_reveal` (default) or `on_listing`; a rank ceiling (`maxRank`) or at least one trait; `maxPricePerItemNative` (`maxPricePerItem` or `price` from the plan); `budgetNative` at least one item's price; `maxItems` 1 to 50 (default 1); `expiresInHours` 1 to 720 (default 72). Missing pieces reply `snipe_details_required` naming the field.
6. The plan is stored `armed` with an idempotency key of profile, chain, slug and run id; the collection's priority is raised and a backfill sync queued so it keeps indexing without a browser.

`validateAgentPlan` refuses a snipe-manage plan in question mode (`action_as_question`), so "should I snipe X?" is answered as research.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.trover.tech/engineering/agent-orchestration.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
