> Current network: **Monad testnet 10143 only**. The assistant creates **price alerts**; it never signs and cannot place an order. Users can confirm new entries and linked protections directly in the chat ticket, using the same browser-held trading key as the perpetual desk. Exits and management remain in the desk. See [testnet configuration](orb-testnet.md).

> Current status reviewed September 29, 2026. See [the release index](README.md) for shipped versus planned features, network, wallet signing and deployment boundaries. Older dated experiments below are retained as history.

# Orb Assistant (OpenRouter Free)

The Orb Assistant turns natural-language trading requests ("what's MON doing?", "alert me if it drops below X") into market reads and **price alerts**. It never receives a private key and never signs. It works on the wallet the profile bound as its Orb Wallet, separate from the login wallet.

## Review and signing

The assistant prepares a draft. The user reviews it in chat; supported onchain steps select the embedded Orb Wallet and avoid a duplicate Privy transaction popup. This does not give the model signing authority or make alerts execute trades. Perpl session enrollment remains separate.

## Safety model

Conversation scope is limited to crypto trading and Orb Wallet support. A separate tool-free classifier runs before the trading loop, with the user's message and bounded conversation context, without wallet credentials. Obvious asset + trading requests skip that extra inference. Only an exact `ALLOW` (plain text or the supported decision JSON) without tool calls admits an ambiguous request; denial or malformed output returns a fixed localized redirect. Provider errors stop the request. The trading system prompt also rejects unrelated and mixed-topic requests and treats tool results as untrusted data. This adds one short inference for ambiguous messages. Model classification is probabilistic and is not a substitute for server authorization. `node scripts/test-assistant-scope.mjs` verifies enforcement with mocked classifications.

The assistant cannot send or sign. Its whitelist is nine tools, each mapped onto validated server logic:

- `get_orb_wallet` → `requireOrbWallet` + `orbWalletPortfolio` (`lib/orb-wallet-server.ts`)
- `get_balance` → on-chain balance for the Orb wallet
- `list_perp_markets` / `get_perp_market` → Perpl's public context through `lib/perpl.ts` (mark, oracle, bid/ask, funding, leverage ceiling and fees, all scaled per market)
- `create_intent` / `list_intents` → `lib/trade-intents.ts`
- `prepare_trade` → validates a complete Perpl ticket (market, side, order type, margin, leverage, entry and protections) and returns a review-only draft. It never signs or submits.
- `get_kuru_spot` → the live Kuru MON/USDC testnet market, wallet balances and an indicative quote (`lib/kuru-spot.ts` path in `lib/trade-assistant.ts`)
- `prepare_spot_trade` → a review-only Spot card returned as `spotDraft` in `AssistantResult`; buy amounts are Kuru USDC, sell amounts are MON, and the Kuru quote asset is not Circle USDC

The perpetual draft arrives as `orderDraft`, the Spot draft as `spotDraft`; both are review-only and neither is submitted by the server.

The wallet is resolved server-side from the person's bound Orb Wallet; the model never supplies or overrides a wallet, recipient or address. Session binding (`tradeSession`, `lib/trade-auth.ts`) and Orb-wallet binding (`requireOrbWallet`/`bindVerifiedOrbWallet`, enforcing a verified non-imported embedded spending wallet and one Orb wallet per profile) are unchanged. Without a bound Orb wallet the route returns the `wallet_setup_required` code and the UI offers Orb-wallet setup instead of trading. `create_intent` answers with a note restating that the alert is a notification and the order is the user's.

## Price alerts

Migration `drizzle/0030_orb_testnet_finance.sql` creates `orb_testnet_trade_intents` — the table the code actually reads and writes (kind `stop_loss`/`take_profit`, `trigger_direction` `above`/`below`, `trigger_target`, `slippage`, `status` `pending`/`triggered`/`done`/`cancelled`/`expired`, `expires`), scoped by person + wallet and distinct from the room-scoped social `orb_strategies` in `0026`. The earlier `orb_trade_intents` from `0027` is superseded and kept only as a historical record; nothing in `lib/` queries it. `lib/trade-intents.ts` validates and clamps inputs (slippage 10/50/100, expiry 60s–7d), caps active alerts, sweeps expired rows on read, and enforces one-directional status transitions with optimistic concurrency.

Detection runs in a separate server worker, even with browsers closed. The fenced lease the worker takes lives in `orb_testnet_intent_monitor`, also created by migration `0030` (the `orb_intent_monitor` table from `0028` is the superseded predecessor). Each cycle selects up to 500 oldest-checked alerts, resolves their current bound spending wallets, deduplicates up to 30 token contracts and takes **one** Perpl context read. MON maps to WMON and the quote stable prices at par; the mark comes from the venue's own MON perpetual. The threshold is output tokens per ONE input token, calculated from both prices using exact BigInt decimal ratios.

**The assistant is no longer the only door.** `app/api/orb/alerts/route.ts` (`tradeSession`, actions `list`/`create`/`cancel`) serves the same rows for the Carteira **Alertas** tab, and `lib/trade-alerts.ts` answers each condition with the venue's current ratio beside it. The wallet is resolved from the profile's own binding, never from the request, so one member cannot list or cancel another's conditions even by guessing an id. A member sets a pair, a side and a price; `kind` and `slippage` stay the ticket shape the desk uses (`above` → `take_profit`, `below` → `stop_loss`, slippage 50) because the row is still the same row. See [Orb Wallet](orb-wallet.md).

Because detection lives in that worker, an instance without `npm run intents:monitor` running never fires anything: the alerts panel says `evaluator stopped` and names the command instead of leaving a condition looking patiently pending.

100 alerts on the same pair share one market request. The database enforces a 10-second cooldown across workers, more markets rotate through subsequent cycles, and failures back off. Missing, invalid or locally stale data cannot trigger. A worker that lost its lease cannot commit. The frontend only reads account state (10 seconds active, 30 seconds idle, paused when hidden).

A triggered alert is a notification, nothing more: the user opens the desk and acts, or lets the alert expire. `fetchedAt` measures retrieval time, not the venue's own update time, and marks can lag a fast market. The UI shows monitor health, last check and missing prices.

## Where the order actually happens

The perpetual desk places market, limit and **trigger** orders over the venue's trading socket, signed by an Ed25519 key generated in the user's browser. A trigger rests on Perpl until the mark crosses its price: as an *entry* it then opens the position as a market order within a slippage cap, and as Perpl's native take-profit / stop-loss it is linked to the entry request, so protection waits with a resting entry and is cancelled if that entry is cancelled. No keeper runs any of this. Because that key never reaches Orb's server, no Orb process — assistant, monitor or otherwise — can trade for a user.

A stop is a trigger, not a guaranteed fill price: on a gap the venue's own slippage bounds decide. An alert's `slippage` is stored for the user to carry over by hand; nothing Orb keeps can bind an onchain floor.

The earlier funded-order path (`contracts/OrbOrderExecutor.sol`, `lib/reserved-orders.ts`, `lib/intent-execution.ts` and their keeper) is deleted: the venue it targeted offered no testnet order endpoint, and the replacement already exists as trigger orders at Perpl. Migrations keep those tables as historical records.

## API and UI

`app/api/orb/assistant/route.ts` (`runtime='nodejs'`, `force-dynamic`) requires `tradeSession` + `requireOrbWallet` and rate-limits per person (chat 20/min, cancellations 60/min). POST actions: `chat`, `market`, `intents`, `cancelIntent`; any other action is rejected. State is `{ configured, assistant: { freePerDay, left, resetsAt, turnsPerReview }, intents, monitor }`, and `left` is the real number of free reviews remaining today (reading it never spends one). Missing inference credentials return 503 for `chat`. Past that gate, `lib/ai-access.ts` meters the inference itself. **A review is a conversation, not a message**: each wallet gets 5 free conversations per UTC day (`ASSISTANT_FREE_REVIEWS_PER_DAY = 5`); the first message of a conversation spends one and returns a `thread` id, and the follow-ups that carry it cost nothing more for up to 12 turns (`ASSISTANT_TURNS_PER_REVIEW`) while the conversation is not left idle for 30 minutes. A thread is keyed by wallet and a random id in `request_limits`, so a made-up or borrowed id buys nothing, and a follow-up never waits for a licence lookup on the chain. Beyond the allowance the request is refused with **402** and a localized explanation unless an active 100 ORB Holder Pro licence covers the member — either their own, or one held by the owner of a community they belong to (their 8 newest memberships are checked). A licence that cannot be read from the chain counts as absent, so an RPC failure never grants free inference. Tool names, count and argument size are bounded independently of model instructions.

**Speed.** The `market` action answers from the venue without a model: it returns one market's mark, oracle, bid/ask, funding, open interest, volume, leverage ceiling, margin requirements and fees, the chat renders it as a card, and it spends no review. An obvious trading request is recognised by a pattern and costs no classification call; anything else is classified before it can reach a tool, an account or the main model. With `stream: true` the chat answer travels as newline-delimited JSON: one `status` event per read or preparation (`Reading the BTC market…`), then a final `result` (or `error`) line. Every check that can refuse the request still happens before the stream opens, so a refusal is an ordinary JSON status.

`components/orb-assistant.tsx` renders the chat thread, the alert review card, the AI order-draft review card and the active-alert list inside the trade workspace (Assistant mode). The newest draft is an editable ticket in the chat. Users change side, order type, margin, leverage, entry, protection prices and slippage, review balance and estimated fee, then confirm there. Edits reset review. A new draft supersedes the previous unsubmitted one. Requests carry at most 10 sanitized messages, 1,600 characters each; history is untrusted context, never authorization. Changing wallet aborts the pending conversation and clears it.

The top-level **Assistant** is now its own navigation destination alongside **Trade** and **Wallet**. Community trade channels retain their contextual AI mode and a short first-use guide. Once a ticket is sent, its card stays mounted to follow Perpl's request and venue updates. The browser keeps a bounded set of recent order updates even after an order leaves the open-order list. The UI distinguishes acknowledgement from an observed partial/full fill or failure, and labels the last observed state after a disconnect. Browser-observed timings are not blockchain finality. The two-person demo and its limits are documented in [orb-two-person-demo.md](orb-two-person-demo.md).

## Configuration

### Community trading channels

Every community trade channel exposes Perps and AI assistant shortcuts. A docked community chat stays alongside the private workspace on wide screens; smaller screens use a closable conversation panel. The current local journey retains private conversation memory per account and room while navigating this browser tab. Global Trade and the Assistant destination share the personal workspace. Different rooms stay separate; switching wallet resets the affected workspace and switching account destroys the cache. Reload closes this memory; nothing is persisted to localStorage.

**My activity** shows recent private drafts and last observed outcomes in that workspace, not full exchange history. A submission is synchronously recorded as unknown before calling Perpl. If navigation interrupts acknowledgement, the same ticket cannot be sent again. Check the venue before creating a new order. Acknowledged requests remain distinct from observed fills; browser observations are not independent chain proof. Users can cancel a slow AI reply; cancellation never rolls back or cancels an already sent financial order. Late AI replies are discarded, and reading earlier conversation no longer forces a scroll to the tail.

Manual and AI tickets offer **Share idea in chat**. The preview names the destination channel and excludes margin by default. Only the validated trade idea is published, not the AI transcript, wallet address, signing material, or execution result. The existing message endpoint enforces membership, channel visibility, write permissions, timeouts and bans. Versioned testnet cards are canonicalized from a whitelist and stored as message bodies, without a database migration. Search and reply previews use a readable card summary.

Recipients can open a card as their own draft inside the channel assistant. Their margin starts empty, current market validation runs again, and review plus explicit confirmation remain required. A shared card is an idea and price snapshot, never proof of a fill. Completed, rejected and unknown submission outcomes remain non-resubmittable when switching between workspace modes.

`node scripts/test-trade-card-api.mjs` exercises the real message route against an isolated SQLite database. `node scripts/test-community-trade-ui.mjs` checks private conversation retention, share preview/retry, room and wallet isolation, recipient margin reset, one-shot submission outcomes and mobile layout with simulated wallet/venue responses. Neither test posts into a real community or sends a transaction.

The shared ticket validator checks market availability, positive amounts, market precision, leverage bounds, slippage, safe integer sizes and protection direction against the entry reference. It runs on both the server draft and browser review. It rejects invalid inputs instead of silently clamping or dropping protection. Prices older than 30 seconds or a failed price refresh block submission.

Short clarification answers such as `10`, `10 AUSD` or `2x` are admitted in the context of the latest trading-field question; a number without trading context still goes through scope classification. The trading model receives both the original request and its own question and must preserve supplied fields, asking only for missing or ambiguous values. `scripts/probe-assistant-live.mjs --margin-followup` exercises that exact flow with real free inference and no order submission.

A narrow deterministic continuation handles unambiguous market-margin answers and explicit leverage edits such as `muda para 3x` / `set leverage to 3x`. Leverage edits only reuse the latest open perpetual ticket, retain its remaining terms, read current market data and run the complete validator. Cancelled, submitted, malformed or Spot ticket context cannot use this shortcut. No shortcut signs an order; review and confirmation still belong to the user. The October 1 real free-provider probe verified draft creation and an edit from 2× to 3× after finding that a model could describe an edit without returning a new ticket.

Submission uses a synchronous one-shot lock. Entry acknowledgement is distinct from a fill. Rejected and unknown acknowledgements remain visible with no automatic retry. If entry succeeds but a linked protection fails, the ticket retains a partial-success warning and the request ID; it never resends the entry. A wallet change or unmount stops further protection submissions. These orders are separate venue requests, not an atomic batch. The bounded chat loop stops immediately after a validated draft instead of requesting an extra generated summary.

Regression checks: `node scripts/test-assistant-trading.mjs` and `node scripts/test-assistant-chat-ui.mjs` cover validation, follow-up context, editing, double-click prevention, insufficient collateral, partial protection failure, rejected/unknown acknowledgements, mobile overflow and wallet changes. Browser tests use simulated wallet and venue responses; they do not spend funds. Optional `node --env-file=.env.local scripts/probe-assistant-live.mjs` exercises actual free inference and public market data with database/account tools disabled. It creates a draft and edits it conversationally, without signing or submitting.

For OpenRouter, use `HUNYUAN_API_BASE_URL=https://openrouter.ai/api/v1`, keep the API key in the server-only `HUNYUAN_API_KEY`, set `HUNYUAN_MODEL=openrouter/free` and `HUNYUAN_FREE_ONLY=true`. The client sends `provider.max_price` caps of zero and requires tool-capable parameters, so a paid route cannot be selected accidentally. OpenRouter may rotate among its free models and can rate-limit shared pools; the app returns a retryable error instead of falling back to paid inference. Never commit inference credentials.

Set `HUNYUAN_API_BASE_URL` (an OpenAI-compatible base; the client appends `/chat/completions`), `HUNYUAN_API_KEY` and a supported `HUNYUAN_MODEL` in the server environment. Until credentials are present the assistant makes no inference calls. No funded transaction is ever used as an automated test, and this integration is pinned to Monad testnet 10143.

`npm run dev:arena` applies local migrations and starts the app and the alert monitor. To start the worker separately, explicitly load the appropriate env file: `node --env-file=/path/to/environment.env scripts/intent-monitor.mjs`. It requires `TURSO_DATABASE_URL` and optional `TURSO_AUTH_TOKEN`; it does not implicitly load production env files. `--once` runs one cycle. It exposes no HTTP endpoint and signs nothing — it has no key to sign with. Apply migration `drizzle/0030_orb_testnet_finance.sql` before starting it: the worker reads `orb_testnet_trade_intents`, leases `orb_testnet_intent_monitor` and joins `orb_testnet_app_wallets`, and that migration creates all three.

`orb-intent-monitor` is installed and running alongside Orb on Oracle. The deploy/orb-oracle service file is its template. It needs a writable work directory, current migrations and esbuild retained as a runtime dependency. The worker reads prices and records alert outcomes; it signs nothing. Monitor health must still be checked after each release.

## Validation

`npm run intents:test` (rules + monitor + assistant), `npm run perpl:test:market`, `npm run perpl:test:session`, `node scripts/test-testnet-isolation.mjs`, `node scripts/test-assistant-scope.mjs`, TypeScript and the production build.

The isolated-database tests cover batching one read across 100 alerts, lease/cooldown/fencing, missing and stale prices, exact decimal thresholds, cancellation races, wallet scope and atomic caps. Isolation checks cover testnet-pinned venue reads. The retired automatic swap keeper is absent; current Kuru Spot is a separate reviewed, client-signed testnet integration. Assistant tests cover the credentials gate, text sanitizing and a bounded tool loop with tool-error containment and forced summary. The daily review allowance and the Holder Pro paywall in `lib/ai-access.ts` are not covered by an automated suite yet; the other checks listed here are. All of it runs on mocked or read-only transport: no paid inference, no signing and no funded transaction. This is not an independent security audit.
