# Aibora Docs
> Original Monvera reference, presented in the Aibora documentation interface.
The articles below preserve the original Monvera project descriptions. References to its API, contracts, Vera and hosted services are not claims that Aibora operates those services. Aibora currently provides a local demonstration.
Original source: https://docs.monvera.best · commit 4389e59918e343f331f0877ef587ff13be192c03.
---
Developer and API: REST API reference, auth, conventions, addresses, verifying Vera
# Developers
> Build on Monvera: authentication, conventions, the network, and how to verify Vera yourself.
A REST API over the same engine the app uses. Market data, the strategy book, and the backtester are public; anything touching an account needs a token. [Quickstart](/dev/quickstart/)A call that works right now, then the authenticated version. [Authentication](/dev/authentication/)Privy bearer tokens, and which routes are public or gated. [Conventions](/dev/conventions/)Base URL, response shapes, rate limits, and every error you can get. [Verify Vera](/dev/verify-vera/)Reproduce a signed plan from chain data and recover her key yourself. [Network and addresses](/dev/network-and-addresses/)Robinhood Chain, USDG, and the contract addresses. [MCP server](/dev/mcp-server/)Point an AI agent at these docs over MCP, or read them as plain text. [Vera on OKX.AI](/dev/vera-on-okx-ai/)Her pay-per-call research listing, priced in USDT over x402. [Vera on Virtuals ACP](/dev/vera-on-virtuals-acp/)Seven analysis services, escrow-paid in USDG on Robinhood Chain. Every route is listed in the [API reference](/dev/api/).
# API overview
> Every Monvera API route with its method, auth requirement, and reference page.
Base URL `https://monvera.best/api`. Auth, rate limits, response envelope, and the full error table: [conventions](/dev/conventions/). **Bearer** routes need a Privy token — see [authentication](/dev/authentication/). ## Market data | Route | Method | Auth | Reference | | ------------- | ------ | ------ | --------------------------------------- | | `/prices` | GET | Public | [Live prices](/dev/api/prices/) | | `/market` | GET | Public | [Market history](/dev/api/market/) | | `/screener` | GET | Public | [Screener](/dev/api/screener/) | | `/themes` | GET | Public | [Theme baskets](/dev/api/themes/) | | `/strategies` | GET | Public | [Strategy book](/dev/api/strategies/) | | `/backtest` | POST | Public | [Backtest a basket](/dev/api/backtest/) | ## Plans | Route | Method | Auth | Reference | | -------------- | ------ | ------ | ------------------------------------------------- | | `/allocate` | POST | Bearer | [Goal to draft plan](/dev/api/allocate/) | | `/quote` | POST | Bearer | [Firm quote](/dev/api/quote/) | | `/commit-plan` | POST | Bearer | [Sign the risk assessment](/dev/api/commit-plan/) | ## Your account | Route | Method | Auth | Reference | | ---------------- | ------------------- | ------ | ----------------------------------------- | | `/portfolio` | GET | Public | [Holdings and value](/dev/api/portfolio/) | | `/activity` | GET | Public | [Invest activity](/dev/api/activity/) | | `/transactions` | GET | Public | [Token transfers](/dev/api/transactions/) | | `/watchlist` | GET · POST | Bearer | [Watchlist](/dev/api/watchlist/) | | `/alerts` | GET · POST · DELETE | Bearer | [Price alerts](/dev/api/alerts/) | | `/notifications` | GET · POST | Bearer | [Notifications](/dev/api/notifications/) | ## Autopilot | Route | Method | Auth | Reference | | ----------------- | ------------------- | ------ | -------------------------------- | | `/autopilot` | GET · POST · DELETE | Bearer | [Autopilot](/dev/api/autopilot/) | | `/autopilot/run` | POST | Bearer | [Autopilot](/dev/api/autopilot/) | | `/autopilot/runs` | GET | Bearer | [Autopilot](/dev/api/autopilot/) | ## Agent | Route | Method | Auth | Reference | | ------------------------------ | ------ | ------ | ---------------------------------------------- | | `/.well-known/agent-card.json` | GET | Public | [Agent card](/dev/api/agent-card/) | | `/vera-record` | GET | Public | [On-chain track record](/dev/api/vera-record/) | | `/pimlico` | POST | Bearer | [Sponsored userOp relay](/dev/api/pimlico/) | Portfolio, activity, and transactions read public chain data for any address — no token needed.
# Read an account's Monvera invest activity
> An account's AI invests recorded on-chain by the executor, newest first.
Returns an account’s Monvera invest activity, newest first: each AI invest that settled through the executor and wrote an `AllocationExecuted` log on chain 4663. Manual buys route through the DEX rather than the executor, so they are not attributable from that log and do not appear here — use [transfers](/dev/api/transactions/) for full history. `GET /api/activity` · public, no token required. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). ## Parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------------------------ | | `address` | address | yes | The account whose invests to read. Missing or invalid returns 400. | ## Returns | Field | Type | Description | | ------------------------ | ------- | ----------------------------------------------------- | | `activity` | array | Recorded invests, newest first. | | `activity[].kind` | string | Always `"invest"`. | | `activity[].usdc` | number | USDG spent, 6-decimal amount as a number. | | `activity[].legCount` | number | Number of legs the invest filled. | | `activity[].txHash` | bytes32 | The transaction that settled and recorded the invest. | | `activity[].blockNumber` | number | Block the invest landed in. | ## Example One invest of the “Steady Growth” basket — Apple, Nvidia, the S\&P 500, US Treasuries — four legs, 100 USDG.
```bash
curl "https://monvera.best/api/activity?address=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
```json
{
"activity": [
{
"kind": "invest",
"usdc": 100,
"legCount": 4,
"txHash": "0x5a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff0",
"blockNumber": 2431902
}
]
}
```
## Errors | Status | Body | When | | ------ | ------------------------------------------------- | ----------------------------------------- | | 400 | `{"error":"A valid wallet address is required."}` | `address` missing or not a valid address. | ## Notes An account with no invests returns an empty `activity` array, not an error. Open any `txHash` at `https://robinhoodchain.blockscout.com/tx/` to read the `AllocationExecuted` log yourself.
# Vera's agent card
> Vera's ERC-8004 discovery document: identity, live agent signer, and registrations.
`GET /.well-known/agent-card.json` returns Vera’s ERC-8004 discovery document — her name, skills, registrations, and the on-chain values an agent or crawler needs to check her, including her live agent signer. `GET /.well-known/agent-card.json` · public, no token · no rate limit · edge-cached `public, s-maxage=3600, stale-while-revalidate=21600`. Base URL and error shapes: [conventions](/dev/conventions/). The card is served dynamically, not from a static file. Two fields are read live from chain 4663 on every request: `metadata.agentSigner` from the VeraRecord contract’s `agentSigner()`, and `metadata.identityRegistry` from chain config. **Read the signer from the card rather than hardcoding it — it can rotate.** If the on-chain read fails, `agentSigner` falls back to the last-known `0xe532105523d4eD559c3a53E3E82D616bE1a085c5` and the request still returns 200. The route takes no parameters. ## Returns | Field | Type | Description | | ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `description`, `image` | string | Vera, what she does, and `https://monvera.best/icon-512.png`. | | `active` | boolean | `true`. | | `persona` | object | `{ role, blurb }`. | | `services` | object | `{ web: { url } }`. | | `endpoints` | object | `{ app, agent, strategies }`, all on `monvera.best`. | | `supportedTrust` | string\[] | `["reputation"]`. | | `skills` | string\[] | Five ids: portfolio-allocation, plain-language-allocation, dca-autopilot, trading-strategy, risk-management. | | `domains` | string\[] | `["finance", "investing", "real-world-assets"]`. | | `x402` | boolean | `false`. This identity is not gated behind an x402 payment. | | `registrations` | object\[] | One entry: `agentId` 58228 in registry `eip155:8453:0x8004a169fb4a3325136eb29fa0ceb6d2e539a432` on Base, registered via Virtuals ACP. | | `metadata` | object | `version`, `chain`, `chainId` (4663), `app`, `demo`, `agentPage`, `explorer`, `identityRegistry`, `agentId`, `agentSigner`, `note`. | ## Example
```bash
curl https://monvera.best/.well-known/agent-card.json
```
```json
{
"name": "Vera",
"description": "AI broker for real tokenized stocks on Robinhood Chain…",
"image": "https://monvera.best/icon-512.png",
"active": true,
"persona": { "role": "your investing assistant", "blurb": "I build plans from real, named companies and funds…" },
"services": { "web": { "url": "https://monvera.best" } },
"endpoints": {
"app": "https://monvera.best",
"agent": "https://monvera.best/agent",
"strategies": "https://monvera.best/api/strategies"
},
"supportedTrust": ["reputation"],
"skills": ["portfolio-allocation", "plain-language-allocation", "dca-autopilot", "trading-strategy", "risk-management"],
"domains": ["finance", "investing", "real-world-assets"],
"x402": false,
"registrations": [
{
"agentId": 58228,
"agentRegistry": "eip155:8453:0x8004a169fb4a3325136eb29fa0ceb6d2e539a432",
"note": "Canonical ERC-8004 identity, registered via Virtuals ACP."
}
],
"metadata": {
"version": "2.3",
"chain": "robinhood-chain",
"chainId": 4663,
"app": "https://monvera.best",
"demo": "https://monvera.best/demo",
"agentPage": "https://monvera.best/agent",
"explorer": "https://robinhoodchain.blockscout.com",
"identityRegistry": "0x751ae640cfa816404b017fbb8234dd21abafbbdc",
"agentId": 1,
"agentSigner": "0xe532105523d4eD559c3a53E3E82D616bE1a085c5",
"note": "Every recommendation carries Vera's EIP-712 RiskInference signature, verified and recorded by VeraRecord in the same transaction as the trades…"
}
}
```
## Two identities, both real The card labels Vera’s Base identity (`8453:58228`, via Virtuals ACP) as canonical, and also lists her as agent #1 in Monvera’s own IdentityRegistry `0x751ae640cfa816404b017fbb8234dd21abafbbdc` on chain 4663. Both exist on-chain; the card documents both rather than picking one. To go from this card to a recovered signer yourself, follow [Verify Vera](/dev/verify-vera/).
# Create and remove price alerts
> List, create, and remove price alerts. Up to 20 active per account.
`GET` lists your alerts, newest first, active and triggered alike. `POST` arms one. `DELETE ?id=N` removes one by id, idempotently. A cron sweep evaluates active alerts and fires each once, [notifying you](/dev/api/notifications/) when the price crosses your threshold. `GET`, `POST`, `DELETE /api/alerts` · requires a Privy bearer token, `401` without one. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). ## Request body (POST) | Field | Type | Required | Description | | ----------- | ------------------------ | -------- | ------------------------------------------------------------------------------ | | `symbol` | string (1 to 12 chars) | yes | A ticker from the 95-asset catalog, per [`GET /api/market`](/dev/api/market/). | | `direction` | `"above"` or `"below"` | yes | Fire when the price rises above or falls below `threshold`. | | `threshold` | number (> 0, ≤ 10000000) | yes | The USD price that arms it. | `DELETE` takes one query parameter, `id`, a positive integer from `GET`. ## Alert fields | Field | Type | Description | | ------------- | ---------------------- | ---------------------------------------- | | `id` | number | Stable id, used for delete. | | `symbol` | string | The ticker the alert watches. | | `direction` | `"above"` \| `"below"` | Which crossing fires it. | | `threshold` | number | The USD price that arms it. | | `active` | boolean | `true` until it fires, then `false`. | | `createdAt` | number | Unix seconds when created. | | `triggeredAt` | number \| null | Unix seconds when it fired, else `null`. | ## Example
```bash
# list
curl https://monvera.best/api/alerts -H "Authorization: Bearer $MONVERA_TOKEN"
# arm: notify when NVDA falls below $120
curl -X POST https://monvera.best/api/alerts \
-H "Authorization: Bearer $MONVERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"symbol":"NVDA","direction":"below","threshold":120}'
# remove
curl -X DELETE "https://monvera.best/api/alerts?id=42" \
-H "Authorization: Bearer $MONVERA_TOKEN"
```
`POST` and `DELETE` return `{"ok":true}`. `GET` returns:
```json
{
"alerts": [
{ "id": 42, "symbol": "AAPL", "direction": "above", "threshold": 250,
"active": true, "createdAt": 1751884800, "triggeredAt": null }
]
}
```
## Errors | Status | Body | When | | ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------- | | 400 | `{"error":"Invalid alert."}` | POST body fails validation: bad `direction`, non-positive `threshold`, or `symbol` over 12 chars. | | 400 | `{"error":"Unknown symbol."}` | `symbol` is not one of the 95 catalog assets. | | 400 | `{"error":"Alert limit reached (20)."}` | You already hold 20 active alerts. | | 400 | `{"error":"Invalid id."}` | DELETE `id` is missing or not a positive integer. | ## Notes The 20-alert cap counts only alerts where `active` is `true`, so a triggered alert frees a slot.
# Turn a goal into a draft plan
> POST /api/allocate turns a plain-language goal and an amount into a draft basket of real assets.
`POST /api/allocate` turns a plain-language goal and a dollar amount into a draft basket: a named allocation over real assets, each leg with a weight, a one-line reason, and a blended risk score. > `POST /api/allocate` · requires a Privy bearer token. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). Nothing is signed and nothing is spent here. Vera reasons over the buyable universe and twelve months of real price history, filters her picks to assets with a live two-way route at the venues, and normalizes weights to sum to 100. A walk-forward backtest against SPY is attached when history allows. Sign the plan afterward with [`POST /api/commit-plan`](/dev/api/commit-plan/). ## Parameters JSON body. | Name | Type | Required | Default | Description | | --------------- | -------------------------------------------------- | -------- | -------------------- | ---------------------------------------------------------------------- | | `goal` | string, 1–600 chars | yes | — | The goal in plain words, e.g. “grow my money steadily for five years”. | | `amountUsd` | number, >0 and ≤1000000 | yes | — | The dollar amount to allocate. | | `riskTolerance` | `"conservative"` \| `"balanced"` \| `"aggressive"` | no | inferred from `goal` | An explicit risk preference. | ## Returns | Field | Type | Description | | ------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `summary` | string | Short plain-language headline for the plan. | | `rationale` | string | Two to four sentences explaining the plan. | | `riskScore` | integer, 0–10000 | Blended portfolio risk in basis points (broad ETFs \~3000–4500, single tech stocks \~5000–7000). | | `allocations` | array | The basket. Each leg is `{ symbol, weightPct, reason }`. Weights sum to 100. | | `backtest` | object \| null | One-year monthly-rebalanced backtest versus SPY, or `null` when under half the weight has usable history. Shape: [`POST /api/backtest`](/dev/api/backtest/). | | `amountUsd` | number | Echo of the requested amount. | | `model` | string | The inference model id that produced the plan. | ## Request
```bash
curl -X POST https://monvera.best/api/allocate \
-H "Authorization: Bearer $MONVERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"goal": "grow my money steadily for five years",
"amountUsd": 100,
"riskTolerance": "balanced"
}'
```
## Example response
```json
{
"summary": "Steady Growth",
"rationale": "A balanced mix built for a five-year horizon. Most of the money sits in broad market exposure and short Treasuries, with two large tech names for growth.",
"riskScore": 4200,
"allocations": [
{ "symbol": "SPY", "weightPct": 40, "reason": "Broad US market in one holding." },
{ "symbol": "SGOV", "weightPct": 30, "reason": "Short US Treasuries, the low-volatility anchor." },
{ "symbol": "AAPL", "weightPct": 15, "reason": "Profitable, cash-rich, steady demand." },
{ "symbol": "NVDA", "weightPct": 15, "reason": "Strong trend, sized small because it swings hard." }
],
"backtest": {
"period": "1Y",
"rebalance": "monthly",
"coveragePct": 100,
"excluded": [],
"portfolio": { "returnPct": 18.4, "volPct": 12.1, "maxDrawdownPct": 9.7, "sharpe": 1.32 },
"benchmark": { "symbol": "SPY", "returnPct": 21.0, "volPct": 13.6, "maxDrawdownPct": 11.2, "sharpe": 1.28 }
},
"amountUsd": 100,
"model": "venice-uncensored"
}
```
`symbol` values render in-app as real names: SPY as “S\&P 500”, SGOV as “US Treasuries”. ## Errors | Status | Body | When | | ------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `{"error":"Invalid request body."}` | Schema validation failed: missing `goal` or `amountUsd`, goal over 600 chars, amount not positive or above 1000000, bad `riskTolerance`. | A `500` from this route usually means the model returned no usable plan, or every pick lacked live liquidity; rephrasing the goal often clears it. A price-history outage never fails the request — it returns `backtest: null` and the plan still comes back. A draft plan is an option with reasons and a risk read, not advice: [is this investment advice](/safety/is-this-investment-advice/). Backtests are [history, not promises](/safety/backtests-are-history-not-promises/).
# Autopilot API
> Set the bounds Autopilot invests inside, trigger a run, and read the run history.
Autopilot invests a set amount on a schedule without you signing each run, but only ever inside four bounds you set. `/api/autopilot` holds the config, `/api/autopilot/run` fires one run now, `/api/autopilot/runs` is the audit trail. `GET`/`POST`/`DELETE /api/autopilot` · `POST /api/autopilot/run` · `GET /api/autopilot/runs` — all require a Privy bearer token. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). The four bounds are `amountUsd`, `cadence`, `riskCeilingBps`, and `maxPerPeriodUsd` (defined below); every run is checked against all four before anything is signed. `walletId` is your Privy embedded-wallet id — granting it lets Monvera’s server sign each run’s spend, and `DELETE` revokes it along with the config. Vera signs the RiskInference risk assessment; the delegated signer signs the spend. ## Read, set, and revoke the config `GET` returns your config or `{"autopilot": null}`. `POST` creates or updates it, resets the schedule, and returns the stored config plus `cadenceSeconds` (`daily` 86400, `weekly` 604800, `biweekly` 1209600, `monthly` 2592000); limited to 20 POSTs per 60 seconds. `DELETE` removes it and returns `{"ok": true}`.
```bash
curl -X POST https://monvera.best/api/autopilot \
-H "Authorization: Bearer $MONVERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"walletId": "wlt_9f2c…",
"owner": "0x1111111111111111111111111111111111111111",
"smartAccount": "0x2222222222222222222222222222222222222222",
"goal": "grow my money steadily for five years",
"amountUsd": 25,
"cadence": "weekly",
"riskCeilingBps": 6000,
"maxPerPeriodUsd": 50
}'
```
| Field | Type | POST | Description | | ----------------- | ---------------------------------------------- | -------- | ------------------------------------------------------------------------------------- | | `walletId` | string | required | Embedded-wallet id the server signs each run’s spend with. Must be yours. | | `owner` | address | required | Embedded EOA that owns the smart account and holds the USDG. | | `smartAccount` | address | required | Smart account that executes the sponsored transaction. | | `goal` | string (1–600 chars) | required | Plain-language goal Vera re-allocates against each run. | | `amountUsd` | number (> 0, ≤ 100000) | required | Bound 1: USD invested per run. | | `cadence` | `daily` \| `weekly` \| `biweekly` \| `monthly` | required | Bound 2: how often a run happens. | | `riskCeilingBps` | integer (0–10000) | optional | Bound 3: assessed risk must stay at or under it. Defaults to `6000`. | | `maxPerPeriodUsd` | number (> 0, ≤ 1000000) | optional | Bound 4: spend cap per period; a run over it is skipped. Defaults to `amountUsd * 2`. | The stored config echoes those fields and adds server-managed ones: `id` (`ap_`), `userId`, `active`, `runs`, Unix seconds `createdAt` / `nextRunAt` / `lastRunAt`, and `spentThisPeriod` — USD deployed this period, which enforces `maxPerPeriodUsd`. | Status | Body | When | | ------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | | 400 | `{"error":"Invalid autopilot settings."}` | Bad `cadence`, non-positive `amountUsd`, malformed address, or `goal` over 600 chars. | | 400 | `{"error":"That wallet does not belong to your account."}` | `walletId` or `owner` is not your own embedded wallet. | ## Trigger a run `POST /api/autopilot/run` takes no body. It runs your saved config now and submits a real on-chain transaction, counting the spend against the current period rather than starting a new one. **Only one run per account can be in flight, and the route is tight-limited to 6 calls per 60 seconds.** A run can take up to two minutes; do not retry on timeout, the in-flight lock will refuse it.
```bash
curl -X POST https://monvera.best/api/autopilot/run \
-H "Authorization: Bearer $MONVERA_TOKEN"
```
Returns `{"ok": true, "txHash": "0xabc…"}` when the run settles on chain 4663, or `{"ok": false, "reason": "…"}` when it was skipped or failed. If the transaction reverts after signing, `reason` is `Run reverted (tx 0x…)` and carries the reverting hash. Bound checks that return `ok: false` before anything is signed: | `reason` | Meaning | | ------------------------------------------------------- | ---------------------------------------------------- | | `Not enough cash for this run.` | USDG balance below `amountUsd`. | | `Period spend cap reached.` | Would push `spentThisPeriod` past `maxPerPeriodUsd`. | | `Plan exceeds your risk ceiling.` | Assessed risk above `riskCeilingBps`. | | `Autopilot is paused.` | Config is not active. | | `Amount too small to split across the plan's holdings.` | A leg falls below the minimum tradable size. | | Status | Body | When | | ------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | 400 | `{"error":"No autopilot is configured."}` | No config; POST one first. | | 429 | `{"error":"Too many requests. Please slow down a moment."}` | Over 6 calls in 60 seconds, or a run is already in flight — that case returns `Retry-After: 30`. | ## List runs `GET /api/autopilot/runs` returns your last 20 runs, newest first. The trail is append-only and records every run — settled, skipped, or errored — so you can reconcile what Autopilot did against what you authorized.
```bash
curl https://monvera.best/api/autopilot/runs \
-H "Authorization: Bearer $MONVERA_TOKEN"
```
```json
{
"runs": [
{
"ranAt": 1751884800,
"amountUsd": 25,
"assessedRiskBps": 4200,
"status": "success",
"txHash": "0xabc123…",
"holdings": [
{ "symbol": "AAPL", "weightPct": 30, "amountUsd": 7.5 },
{ "symbol": "SGOV", "weightPct": 15, "amountUsd": 3.75 }
]
},
{ "ranAt": 1751280000, "amountUsd": 25, "status": "skipped", "reason": "Not enough cash for this run." }
]
}
```
Each record also carries `userId`. `status` is `success`, `skipped`, or `error`; `txHash` and `holdings` appear only on `success`. That `txHash` is the checkable end of the record: one transaction on chain 4663 that both bought the holdings and recorded Vera’s signed risk assessment. Open it on [Blockscout](https://robinhoodchain.blockscout.com).
# Backtest a basket over the last 12 months
> Simulate a weighted basket over 12 months, monthly rebalanced, against buy-and-hold SPY.
`POST /api/backtest` takes a weighted basket and returns its 12-month simulated performance against buy-and-hold SPY. It reads public equity history only — no inference, no user funds. > `POST /api/backtest` · public, no token required · 20 requests per 60 seconds per IP. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). The engine starts at 100, renormalizes weights over the covered subset, drifts positions with daily closes, and rebalances every \~21 trading days. ## Request body `application/json`. | Field | Type | Required | Description | | ------------------------- | ------ | -------- | ------------------------------------------------------------------------- | | `allocations` | array | yes | 1 to 20 legs. | | `allocations[].symbol` | string | yes | A tradable ticker, for example `AAPL`. Unknown symbols return 400. | | `allocations[].weightPct` | number | yes | Positive, up to 100. Weights renormalize over the legs that have history. | ## Response | Field | Type | Description | | ---------------------- | -------------- | ------------------------------------------------------------------------------------------------------------ | | `backtest` | object \| null | `null` when covered weight is below 50% or SPY history is missing. | | `backtest.period` | string | Always `"1Y"`. | | `backtest.rebalance` | string | Always `"monthly"`. | | `backtest.coveragePct` | number | Share of basket weight that had real history to test (0 to 100). | | `backtest.excluded` | string\[] | Symbols left out because no honest public series exists. | | `backtest.portfolio` | object | Simulated basket: `returnPct`, `volPct`, `maxDrawdownPct`, `sharpe`, `curve` (60 points, normalized to 100). | | `backtest.benchmark` | object | Same stats for buy-and-hold SPY, plus `symbol: "SPY"`. | ## Example
```bash
curl https://monvera.best/api/backtest \
-H "Content-Type: application/json" \
-d '{
"allocations": [
{ "symbol": "AAPL", "weightPct": 35 },
{ "symbol": "NVDA", "weightPct": 25 },
{ "symbol": "SPY", "weightPct": 25 },
{ "symbol": "SGOV", "weightPct": 15 }
]
}'
```
The `curve` arrays hold 60 points each, trimmed here for space.
```json
{
"backtest": {
"period": "1Y",
"rebalance": "monthly",
"coveragePct": 100,
"excluded": [],
"portfolio": {
"returnPct": 22.41,
"volPct": 18.9,
"maxDrawdownPct": 12.07,
"sharpe": 1.18,
"curve": [100, 101.3, 99.8, 104.2, 108.6, 115.9, 122.41]
},
"benchmark": {
"symbol": "SPY",
"returnPct": 14.22,
"volPct": 12.68,
"maxDrawdownPct": 8.31,
"sharpe": 1.05,
"curve": [100, 100.9, 100.1, 102.8, 105.4, 110.7, 114.22]
}
}
}
```
## Errors | Status | Body | Cause | | ------ | ----------------------------------- | ----------------------------------------------------------------------------------------------- | | 400 | `{"error":"Invalid request body."}` | Malformed JSON, non-array `allocations`, unknown `symbol`, or a `weightPct` outside 0 to 100. | | 200 | `{"backtest": null}` | Covered weight fell below 50%, or SPY history was missing. Check for null before reading stats. | On a non-null result, `excluded` names any legs dropped for having no public series. History, not a promise A backtest shows what a fixed rule would have done on past prices. It is not a forecast. See [Backtests are history, not promises](/safety/backtests-are-history-not-promises/).
# Get balance history
> Hourly balance snapshots for any address — the equity curve behind the app's Total balance chart.
`GET /api/balance-history?address=0x…` returns hourly balance snapshots for an address — cash, invested, and total in USD, oldest first. This is the data behind the app’s Total balance chart: a real equity curve that captures deposits, sends, and trades, not just market movement. > `GET /api/balance-history` · public, no token required · 120 requests per 60 seconds per IP, cached 5 minutes at the edge. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). Snapshots are taken hourly for every address the app has seen, valued through the same code as [`/api/portfolio`](/dev/api/portfolio/), and kept for 90 days. Like the portfolio route, it is public because it reveals only what the chain already shows for an address. ## Parameters | Param | Type | Description | | --------- | ------ | ------------------------------------------------------------- | | `address` | string | The wallet to read. Required, checksummed or lowercase. | | `hours` | number | Window to return, 2 to 2160 (90 days). Default 720 (30 days). | ## Response `{ snapshots, asOf }`. Each snapshot: `takenAt` (unix seconds, on the hour), `cashUsd`, `investedUsd`, `totalUsd`. An address the app has never seen returns an empty list — snapshots begin accruing after first sign-in. ## Example
```bash
curl -s "https://monvera.best/api/balance-history?address=0xc6d7709dd8ba53832bd578a88260f8b8e59fb4c7&hours=48" | jq '.snapshots | length'
```
# Sign a plan's risk assessment
> POST /api/commit-plan returns the risk assessment Vera signs for a plan, for on-chain recording.
`POST /api/commit-plan` builds the `RiskInference` for a draft plan and returns it signed by Vera’s agent key. This signature is the accountability record: Vera signs the risk assessment, and the client records it on-chain in the same transaction that buys the assets. The user separately signs the spend. > `POST /api/commit-plan` · requires a Privy bearer token. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). This route signs and returns — it does not broadcast and does not return a transaction hash. The client calls `VeraRecord.record(planId, recHash, assessedRisk, maxRisk, expiry, signature, user, agentId, usdSpent, legCount)` and batches that with the venue settlement calls into one gas-sponsored ERC-4337 userOp, relayed via [`POST /api/pimlico`](/dev/api/pimlico/). Because the record and the buys share one atomic transaction, the assessment cannot be edited after the fact. ## Parameters JSON body. | Name | Type | Required | Description | | ------------ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `address` | string (address) | yes | The smart account that will own the holdings and submit the invest transaction. | | `allocation` | object | yes | The draft plan to sign, in the exact shape returned by [`POST /api/allocate`](/dev/api/allocate/): `{ summary, rationale, riskScore, allocations[] }`. | | `amountUsd` | number, >0 and ≤1000000 | yes | The dollar amount being invested. | ## Returns | Field | Type | Description | | -------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | | `planId` | string (bytes32) | keccak256 of the allocation plus the request timestamp. Single-use — the on-chain record rejects a replayed `planId`. | | `recHash` | string (bytes32) | keccak256 of the canonical allocation JSON, the recommendation commitment recorded on-chain. | | `assessedRisk` | integer, 0–10000 | Vera’s assessed portfolio risk in basis points, clamped from `allocation.riskScore`. | | `maxRisk` | integer, 0–10000 | The risk ceiling, `assessedRisk + 1500`, capped at 10000. On-chain verify reverts if `assessedRisk` exceeds it. | | `expiry` | string (uint256 seconds) | Unix expiry, 15 minutes from signing. On-chain verify reverts past it. | | `signature` | string (65-byte hex) | Vera’s EIP-712 signature over the `RiskInference`, recoverable to `agentSigner()`. | | `agentId` | string | Vera’s agent id in Monvera’s IdentityRegistry, `"1"`. | The signed type is `RiskInference(bytes32 planId, uint16 assessedRisk, uint16 maxRisk, uint256 expiry)` over the EIP-712 domain `{ name: "VeraRecord", version: "1", chainId: 4663, verifyingContract: 0x7ff1a5ee19330c165146488a7ad8af6cb41da1df }`. Verify any signature yourself: [verify Vera](/dev/verify-vera/). ## Request
```bash
curl -X POST https://monvera.best/api/commit-plan \
-H "Authorization: Bearer $MONVERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"address": "0x4D2b1c3A5e6F7089aB0cD1e2F3a4B5c6D7e8F901",
"amountUsd": 100,
"allocation": {
"summary": "Steady Growth",
"rationale": "A balanced five-year mix of broad market exposure, short Treasuries, and two large tech names.",
"riskScore": 4200,
"allocations": [
{ "symbol": "SPY", "weightPct": 40, "reason": "Broad US market in one holding." },
{ "symbol": "SGOV", "weightPct": 30, "reason": "Short US Treasuries, the low-volatility anchor." },
{ "symbol": "AAPL", "weightPct": 15, "reason": "Profitable, cash-rich, steady demand." },
{ "symbol": "NVDA", "weightPct": 15, "reason": "Strong trend, sized small because it swings hard." }
]
}
}'
```
## Example response
```json
{
"planId": "0x8f2a1c9b7d3e4f5a6b8c0d1e2f3a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6",
"recHash": "0x3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b",
"assessedRisk": 4200,
"maxRisk": 5700,
"expiry": "1751991300",
"signature": "0x2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b1b",
"agentId": "1"
}
```
## Errors | Status | Body | When | | ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `400` | `{"error":"Invalid request body."}` | Schema validation failed: bad `address`, malformed `allocation`, non-positive amount or amount above 1000000. | ## The signature is time-boxed Sign here but fail to submit the invest within the 15-minute `expiry` and the on-chain verify reverts with `InferenceExpired()` — no assets are bought. Fetch a fresh signature and settle promptly. The other on-chain reverts are `RiskCeilingBreached(assessed, maxRisk)`, `BadSigner(recovered)`, and `PlanAlreadyRecorded(planId)`, documented on [verify Vera](/dev/verify-vera/).
# Get the Groves
> Every curated basket with live prices, on-chain stats, and a one-year backtest — the same data the Grove pages render.
`GET /api/groves` returns every Grove — composition, weights, exclusions, methodology, live component prices, on-chain stats, and a one-year backtest against SPY. `GET /api/groves/{id}` returns one Grove in the same shape. This is exactly what [monvera.best/groves](https://monvera.best/groves) renders. > `GET /api/groves` · `GET /api/groves/{id}` · public, no token required · 120 requests per 60 seconds per IP, cached 60s at the edge. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). ## Parameters `{id}` is the Grove’s lowercase id (`tayyib`, `titan`, `silic`, `rails`). Unknown ids return `404`. ## Response `{ asOf, note, groves }` (the single-Grove route returns the Grove object directly). Each Grove: | Field | Type | Description | | -------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------- | | `id`, `ticker`, `name`, `category`, `thesis` | string | Identity and one-line thesis. | | `components` | array | Every holding: `symbol`, `weightBps` (sums to exactly 10000), `reason`, `name`, live `priceUsd` (or `null`). | | `excluded` | array | Names deliberately kept out, each with its `why` — including names dropped for missing a two-way venue route. | | `feeBps` | number | The exit fee in basis points (1000 = 10% of realized profit, the only fee). | | `minBuyUsd`, `recommendedUsd` | number | The minimum and comfortable buy sizes. | | `stats` | object | `deployed`, `users`, `managedUsd`, `feesUsd` — read from the Grove contract, honest zeros before deployment. | | `backtest` | object \| null | One-year walk vs buy-and-hold SPY. History, not a promise. | ## Example
```bash
curl -s https://monvera.best/api/groves/titan | jq '{name, minBuyUsd, weights: [.components[] | {symbol, weightBps}]}'
```
# Get market history for charts
> Per-asset day summary with sparklines, or a full close series for one symbol and range.
`GET /api/market` returns real market history for charts. With no `symbol` it returns a one-day change and sparkline for every asset. With a `symbol` it returns the full closing-price series for one range, plus asset-level metadata. > `GET /api/market` · public, no token required · 60 requests per 60 seconds per IP. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). ## Parameters | Name | Type | Required | Default | Description | | -------- | ------ | -------- | ------- | ----------------------------------------------------------------------------------------- | | `symbol` | string | No | (none) | A ticker in the universe, for example `AAPL`. Omit it for the day summary of every asset. | | `range` | string | No | `1M` | One of `1D`, `1W`, `1M`, `1Y`, `All`. Symbol mode only. | ## Response **Summary mode** (no `symbol`): `{ summary, asOf }`. `summary` maps each asset symbol to `{ dayChangePct, spark }`, where `spark` is a downsampled 1-day close series (up to 20 points) for row sparklines. Assets with no live source have no entry. **Symbol mode**: `{ series, timestamps, changePct, meta, asOf }`. | Field | Type | Description | | ------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `series` | number\[] \| null | Closing prices in USD, oldest to newest. `null` when no source exists or the upstream failed. | | `timestamps` | number\[] \| null | Unix seconds per close, aligned to `series`. | | `changePct` | number \| null | Percent change across the range. `1D` is measured against the previous session close. | | `meta` | object \| null | Asset facts: `fiftyTwoWeekHigh`, `fiftyTwoWeekLow`, `dayHigh`, `dayLow`, `volume`, `exchange`. Fields the source did not provide are omitted. | | `asOf` | string | ISO timestamp of the response. | ## Example
```bash
# Day summary for every asset
curl "https://monvera.best/api/market"
# One-year series for Apple
curl "https://monvera.best/api/market?symbol=AAPL&range=1Y"
```
Summary mode:
```json
{
"summary": {
"AAPL": { "dayChangePct": 0.82, "spark": [228.4, 228.9, 229.1, 230.2, 231.0] },
"NVDA": { "dayChangePct": -1.14, "spark": [174.2, 173.6, 172.9, 172.1, 171.8] }
},
"asOf": "2026-07-08T14:03:11.204Z"
}
```
Symbol mode:
```json
{
"series": [188.4, 191.2, 195.7, 201.3, 231.0],
"timestamps": [1720396800, 1723075200, 1725753600, 1728345600, 1751983200],
"changePct": 22.61,
"meta": {
"fiftyTwoWeekHigh": 242.1,
"fiftyTwoWeekLow": 169.2,
"dayHigh": 231.4,
"dayLow": 228.0,
"volume": 41283900,
"exchange": "NasdaqGS"
},
"asOf": "2026-07-08T14:03:11.204Z"
}
```
## Errors | Status | Body | Cause | | ------ | ----------------------------- | ----------------------------------------------- | | 400 | `{"error":"Unknown symbol."}` | `symbol` is not in the universe. | | 400 | `{"error":"Unknown range."}` | `range` is not one of the five accepted values. |
# Read the inbox and mark it read
> Read the signed-in account's inbox and unread count, or mark everything read.
`GET` returns the signed-in account’s notifications (newest first, up to 50) and the current unread count in one response. `POST` marks every unread notification read, takes no body, and returns `{"ok":true}` — after it runs, `unread` on the next `GET` is `0`. Notifications come from price alerts firing, Autopilot runs, and completed trades. `GET`, `POST /api/notifications` · requires a Privy bearer token, `401` without one. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). ## Returns (GET) | Field | Type | Description | | --------------- | ------ | ------------------------------------------------ | | `notifications` | array | Up to 50 notifications, newest first. | | `unread` | number | Count of notifications with `readAt` still null. | Each notification: | Field | Type | Description | | ----------- | ----------------------------------------------------- | --------------------------------------------------- | | `id` | number | Stable id. | | `kind` | `"alert"` \| `"autopilot"` \| `"trade"` \| `"system"` | What produced it. | | `title` | string | One-line summary. | | `body` | string \| undefined | Optional detail line. | | `symbol` | string \| undefined | The related ticker, when there is one. | | `txHash` | string \| undefined | The settling transaction hash, when there is one. | | `createdAt` | number | Unix seconds when created. | | `readAt` | number \| undefined | Unix seconds when marked read, absent while unread. | ## Example
```bash
# read the inbox
curl https://monvera.best/api/notifications \
-H "Authorization: Bearer $MONVERA_TOKEN"
# mark everything read
curl -X POST https://monvera.best/api/notifications \
-H "Authorization: Bearer $MONVERA_TOKEN"
```
```json
{
"notifications": [
{
"id": 1807,
"kind": "autopilot",
"title": "Autopilot invested $25.00",
"body": "Vera placed your scheduled plan. Tap to see the run.",
"txHash": "0x…",
"createdAt": 1751884800,
"readAt": null
},
{
"id": 1806,
"kind": "alert",
"title": "NVDA fell below $120",
"symbol": "NVDA",
"createdAt": 1751880000,
"readAt": 1751881000
}
],
"unread": 1
}
```
## Notes `txHash` is the checkable end of an autopilot or trade notification: it names the exact transaction on Robinhood Chain (4663) that settled it. Open it at `https://robinhoodchain.blockscout.com/tx/`. Neither method is rate limited. See also [alerts](/dev/api/alerts/) and [autopilot](/dev/api/autopilot/).
# Relay a gas-sponsored user operation
> The token-gated ERC-4337 bundler and paymaster relay that sponsors the gas for an invest.
`POST /api/pimlico` is the ERC-4337 bundler and paymaster relay that pays the gas for a Monvera invest. It forwards a whitelisted set of JSON-RPC methods to Pimlico with the server-held API key attached, so the browser never sees the key. This is why investing is free to the user: the paymaster covers the network cost. `POST /api/pimlico` · requires a Privy bearer token · 600 requests per 60 seconds per account. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). You usually do not call this directly: the smart-account client points its bundler and paymaster transport here, so the ERC-4337 stack calls it for you during an invest. Call it yourself only if you are building your own account-abstraction transport against Monvera. ## What it returns The upstream Pimlico JSON-RPC envelope, unchanged. The relay accepts a single JSON-RPC object or a batch array, checks every `method` against the allowlist, forwards allowed requests to `https://api.pimlico.io/v2/4663/rpc`, and returns Pimlico’s body and status verbatim. Upstream JSON-RPC errors come back as-is for your RPC layer to interpret; upstream headers are never leaked. ## Allowed methods Any method outside this set is rejected with `400`, and an empty batch is rejected too.
```plaintext
eth_chainId
eth_supportedEntryPoints
eth_estimateUserOperationGas
eth_sendUserOperation
eth_getUserOperationByHash
eth_getUserOperationReceipt
pimlico_getUserOperationGasPrice
pimlico_getUserOperationStatus
pm_sponsorUserOperation
pm_getPaymasterData
pm_getPaymasterStubData
```
## Parameters The body is a raw JSON-RPC 2.0 payload — a single object or a batch array. There is no Monvera-specific schema beyond the allowlist. | Field | Type | Required | Description | | --------- | ---------------- | -------- | --------------------------------------------------------------------- | | `jsonrpc` | string | yes | `"2.0"`. | | `id` | number \| string | yes | Request id, echoed in the response. | | `method` | string | yes | One of the allowed methods above. | | `params` | array | yes | The method’s parameters, per the ERC-4337 bundler and paymaster spec. | ## Example Read the current user-operation gas price — a safe, side-effect-free call:
```bash
curl -X POST https://monvera.best/api/pimlico \
-H "Authorization: Bearer $MONVERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"pimlico_getUserOperationGasPrice","params":[]}'
```
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"slow": { "maxFeePerGas": "0x5f5e100", "maxPriorityFeePerGas": "0x5f5e100" },
"standard": { "maxFeePerGas": "0x77359400", "maxPriorityFeePerGas": "0x77359400" },
"fast": { "maxFeePerGas": "0x8f0d1800", "maxPriorityFeePerGas": "0x8f0d1800" }
}
}
```
## Errors | Status | Body | When | | ------ | ----------------------------------------------------- | ----------------------------------------------- | | 400 | `{"error":"Invalid JSON-RPC body."}` | The body is not valid JSON. | | 400 | `{"error":"Unsupported RPC method."}` | No method, or any method outside the allowlist. | | 502 | `{"error":"Something went wrong. Please try again."}` | The upstream Pimlico call failed. | | 503 | `{"error":"Something went wrong. Please try again."}` | No Pimlico API key configured on the server. | There is no open gas-sponsorship relay: a missing or bad token is always a `401`. A `200` only means the relay forwarded your request — a userOp can still fail inside the JSON-RPC `result` or `error` object, so check the envelope. The invest itself is atomic on-chain: if the batched `VeraRecord.record(...)` verify reverts, the whole userOp reverts and no assets are bought.
# Read an account's holdings and value
> Cash, positions, and total value for any address, priced server-side.
Returns the fully valued holdings for any account address: USDG cash, every non-zero position with its live price and 1-day move, and the totals. One server-side multicall reads the balances, and prices come from the same spot source as `/api/prices`, so the response needs no client-side math. `GET /api/portfolio` · public, no token required. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). ## Parameters | Name | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------- | | `address` | address | yes | The account to value. Missing or invalid returns 400. | ## Returns | Field | Type | Description | | ------------------------- | ----------------- | ------------------------------------------------------- | | `cashUsd` | number | USDG balance (6-decimal), as a number. | | `investedUsd` | number | Sum of priced holding values. | | `totalUsd` | number | `cashUsd + investedUsd`. | | `holdings` | array | Non-zero positions, largest value first, unpriced last. | | `holdings[].symbol` | string | Registry symbol, for example `AAPL`. | | `holdings[].raw` | string | Raw token balance as a decimal string (bigint-safe). | | `holdings[].qty` | number | Human quantity, token decimals applied. | | `holdings[].priceUsd` | number \| null | Live spot price, `null` when unpriced. | | `holdings[].valueUsd` | number \| null | `qty * priceUsd`, `null` when unpriced. | | `holdings[].dayChangePct` | number \| null | 1-day move for the row. | | `holdings[].spark` | number\[] \| null | Intraday sparkline points. | | `asOf` | string | ISO timestamp the snapshot was built. | ## Example
```bash
curl "https://monvera.best/api/portfolio?address=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
```json
{
"cashUsd": 42.5,
"investedUsd": 207.5,
"totalUsd": 250,
"holdings": [
{
"symbol": "AAPL",
"raw": "540000000000000000",
"qty": 0.54,
"priceUsd": 227.5,
"valueUsd": 122.85,
"dayChangePct": 0.83,
"spark": [226.1, 226.9, 227.5]
},
{
"symbol": "NVDA",
"raw": "610000000000000000",
"qty": 0.61,
"priceUsd": 138.75,
"valueUsd": 84.64,
"dayChangePct": -1.12,
"spark": [140.3, 139.1, 138.75]
}
],
"asOf": "2026-07-08T14:32:07.918Z"
}
```
## Errors | Status | Body | When | | ------ | -------------------------------------- | ----------------------------------------- | | 400 | `{"error":"Valid ?address required."}` | `address` missing or not a valid address. | ## Notes An account holding nothing returns zeros and an empty `holdings` array, not an error. A single unreadable token is skipped rather than failing the whole response, and its `priceUsd` and `valueUsd` come back `null`.
# Get live USD prices
> Current on-chain USD spot for every asset, read from Chainlink feeds on chain 4663.
`GET /api/prices` returns the current USD spot for every asset, read on-chain from each token’s Chainlink price feed. Assets with no live feed report `source: "none"` and omit `priceUsd` rather than showing a faked number. > `GET /api/prices` · public, no token required · 60 requests per 60 seconds per IP. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). Feeds use 8 decimals and are already corporate-action adjusted (splits, dividends), so the feed value is the token’s full USD price. Every read folds into one `eth_call` through Multicall3 on chain 4663. ## Parameters None. The route prices the whole universe on every call. ## Response `{ prices, asOf }`. `asOf` is the ISO timestamp of the response. `prices` maps each asset symbol to an `AssetPrice`: | Field | Type | Description | | ---------- | ------ | -------------------------------------------------------------------------------------- | | `symbol` | string | The asset ticker. | | `priceUsd` | number | USD per whole token. Omitted when the asset has no live feed or the price is stale. | | `source` | string | `"chainlink"` when a fresh feed answered, `"none"` when there is no usable live price. | ## Example
```bash
curl "https://monvera.best/api/prices"
```
```json
{
"prices": {
"AAPL": { "symbol": "AAPL", "priceUsd": 231.02, "source": "chainlink" },
"NVDA": { "symbol": "NVDA", "priceUsd": 171.84, "source": "chainlink" },
"SPY": { "symbol": "SPY", "priceUsd": 614.03, "source": "chainlink" },
"SPCX": { "symbol": "SPCX", "source": "none" }
},
"asOf": "2026-07-08T14:03:11.204Z"
}
```
The `SPCX` entry is the shape for an asset with no live feed: `source: "none"` and no `priceUsd` key.
# Get a firm price for one asset
> POST /api/quote races the live venues for one asset in USDG and returns the winning executable quote, plus the intent to sign.
`POST /api/quote` quotes every live venue (LI.FI, Uniswap V4 and KyberSwap) in parallel for buying or selling one asset in USDG, returns the biggest fill, and includes the typed data the taker signs plus the settlement calls to submit. > `POST /api/quote` · requires a Privy bearer token. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). Buy means USDG to the asset token; sell means the asset token to USDG. Quotes are short-lived — fetch one right before you settle, because the typed data in `toSign` carries its own deadline. ## Parameters JSON body. | Name | Type | Required | Description | | ------------ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `"price"` \| `"quote"` | yes | `"price"` for an indicative amount only; `"quote"` for a firm quote with the signable Permit2 intent and settlement call. | | `side` | `"buy"` \| `"sell"` | yes | `"buy"` sells USDG for the asset; `"sell"` sells the asset for USDG. | | `symbol` | string, 1–12 chars | yes | The asset ticker, e.g. `AAPL`. Unknown tickers return `400`. | | `sellAmount` | string (decimal digits) | yes | Raw base units of the sell token. On a buy that is USDG (6 decimals), so `$25` is `"25000000"`. On a sell it is the 18-decimal asset token. | | `taker` | string (address) | yes | The smart account that will sign and settle. | ## Returns For `mode: "quote"` with liquidity available: | Field | Type | Description | | -------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `liquidityAvailable` | boolean | `true` when a firm quote was found. `false` returns an object with only this field. | | `buyAmount` | string | Raw base units received (18-decimal asset token on a buy, 6-decimal USDG on a sell). | | `minBuyAmount` | string | Minimum received after slippage (default 100 bps). | | `needsAllowance` | boolean | `true` if the taker must approve the sell token to Permit2 before settling. | | `permit2` | string | The Permit2 spender, `0x000000000022D473030F116dDEE9F6B43aC78BA3`. | | `sellToken` | string | The sell token address the quote is priced against. | | `sellAmount` | string | Echo of the raw sell amount. | | `toSign` | object | The Permit2 `PermitWitnessTransferFrom` EIP-712 typed data. Pass it straight to `signTypedData`. | | `tx` | object | The settlement call, `{ to, data, value, signatureOffset }`. Splice the taker’s signature into `data` at `signatureOffset`, then submit. | `mode: "price"` responds with `{ liquidityAvailable, buyAmount }` only. ## Request Buy $25 of AAPL:
```bash
curl -X POST https://monvera.best/api/quote \
-H "Authorization: Bearer $MONVERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "quote",
"side": "buy",
"symbol": "AAPL",
"sellAmount": "25000000",
"taker": "0x4D2b1c3A5e6F7089aB0cD1e2F3a4B5c6D7e8F901"
}'
```
## Example response
```json
{
"liquidityAvailable": true,
"buyAmount": "97640000000000000",
"minBuyAmount": "96663600000000000",
"needsAllowance": false,
"permit2": "0x000000000022D473030F116dDEE9F6B43aC78BA3",
"sellToken": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
"sellAmount": "25000000",
"toSign": {
"domain": { "name": "Permit2", "chainId": 4663, "verifyingContract": "0x000000000022D473030F116dDEE9F6B43aC78BA3" },
"types": { "PermitWitnessTransferFrom": [] },
"message": { "deadline": "1751990400" }
},
"tx": {
"to": "0x117cc2133c37B721F49dE2A7a74833232B3B4C0C",
"data": "0x...",
"value": "0",
"signatureOffset": 456
}
}
```
`buyAmount` is the AAPL token amount (18 decimals); `sellToken` is USDG. ## Errors | Status | Body | When | | ------ | ----------------------------------- | -------------------------------------------------------------------------------------------------------- | | `400` | `{"error":"Invalid request body."}` | Schema validation failed: missing field, bad `mode`/`side`, non-numeric `sellAmount`, malformed `taker`. | | `400` | `{"error":"Unknown symbol: XYZ"}` | The `symbol` is not in the catalog. Valid tickers: [`GET /api/market`](/dev/api/market/). | `{"liquidityAvailable": false}` is a `200`, not an error: the asset is known but has no firm executable quote right now. Treat it as “cannot trade this leg at the moment” and sign nothing.
# Rank the universe by performance
> Every buyable asset with real 12-month return, volatility and drawdown figures, ready to sort.
`GET /api/screener` returns every buyable asset with four figures computed from 12 months of real price history, so you can rank the universe instead of guessing. Assets with no honest public history are dropped rather than ranked on a faked series. > `GET /api/screener` · public, no token required · 60 requests per 60 seconds per IP. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). The screener shares its cached computation with Vera’s allocation prompt, so a page full of clients costs at most one upstream build per window. ## Parameters None. The route returns the whole buyable set. ## Response `{ assets, asOf }`. `asOf` is the ISO timestamp of the response. `assets` is an array of stat rows, one per buyable asset with usable history: | Field | Type | Description | | ---------------- | -------------- | --------------------------------------------------------------------------- | | `symbol` | string | The asset ticker, for example `NVDA`. | | `ret1yPct` | number \| null | Total return over the last \~12 months, percent. | | `ret3mPct` | number \| null | Return over the last \~3 months, percent. `null` when history is too short. | | `volPct` | number | Annualized daily volatility, percent. | | `maxDrawdownPct` | number | Worst peak-to-trough drop over the period, percent (a positive number). | | `noData` | boolean | Always `false` in the response. | ## Example
```bash
curl "https://monvera.best/api/screener"
```
```json
{
"assets": [
{
"symbol": "NVDA",
"ret1yPct": 38.42,
"ret3mPct": 9.61,
"volPct": 41.27,
"maxDrawdownPct": 22.84,
"noData": false
},
{
"symbol": "SGOV",
"ret1yPct": 4.91,
"ret3mPct": 1.24,
"volPct": 0.41,
"maxDrawdownPct": 0.06,
"noData": false
}
],
"asOf": "2026-07-08T14:03:11.204Z"
}
```
Sort client-side on whichever column you care about:
```javascript
const { assets } = await (await fetch("https://monvera.best/api/screener")).json();
const ranked = [...assets].sort((a, b) => (b.ret1yPct ?? 0) - (a.ret1yPct ?? 0));
```
# Get the open strategies and backtests
> Deterministic rule-based baskets with their weights and a walk-forward backtest each.
`GET /api/strategies` returns Monvera’s public strategy book: rule-based baskets computed from real 12-month price data, each with its plain-language rule, its current weights, and a walk-forward backtest. No AI is in this loop, so the same data always produces the same weights and anyone can reproduce them. This is the raw JSON behind [monvera.best/strategies](https://monvera.best/strategies). > `GET /api/strategies` · public, no token required · 30 requests per 60 seconds per IP. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). ## Parameters None. ## Response `{ asOf, refreshedEvery, note, strategies }`. | Field | Type | Description | | ---------------- | --------- | ------------------------------------------------------------------------------------------------ | | `asOf` | string | ISO timestamp of the build. | | `refreshedEvery` | string | `"6h"`. Strategies are rebuilt from fresh market data every six hours. | | `note` | string | The methodology statement, including that backtests are walk-forward and history, not a promise. | | `strategies` | object\[] | The strategy list (below). | Each strategy: | Field | Type | Description | | ------------- | -------------- | ------------------------------------------------------------------------- | | `id` | string | Stable id: `steady-foundation`, `momentum-leaders`, or `balanced-growth`. | | `name` | string | Display name, for example `Momentum Leaders`. | | `tagline` | string | One-line summary. | | `method` | string | The full rule in plain words, enough to reproduce the weights. | | `allocations` | object\[] | `{ symbol, weightPct }` per holding; integer weights summing to 100. | | `backtest` | object \| null | The walk-forward backtest, or `null` if a data gap prevented one. | Each `backtest`: `period` (`"6M"`), `methodology` (`"walk-forward"`), `rebalance` (`"monthly"`), `portfolio`, and `benchmark` (a `BacktestSeries` with `symbol: "SPY"`). A `BacktestSeries` carries `returnPct`, `volPct`, `maxDrawdownPct`, `sharpe`, and a downsampled `curve` normalized to start at 100. ## Example
```bash
curl "https://monvera.best/api/strategies"
```
The live response carries all three strategies; one is shown here. Weights and backtest figures are illustrative of the shape — call the route for the current book.
```json
{
"asOf": "2026-07-08T14:03:11.204Z",
"refreshedEvery": "6h",
"note": "Deterministic rules over real daily closes. Backtests are WALK-FORWARD, monthly rebalance, vs buy-and-hold SPY over the same window. History, not a promise: markets change.",
"strategies": [
{
"id": "momentum-leaders",
"name": "Momentum Leaders",
"tagline": "The 8 strongest uptrends, sized so no single name can sink it.",
"method": "Universe: all tokenized stocks with 12 months of public history. Filter: 1-year AND 3-month returns both positive. Rank: 0.7 x 1-year return + 0.3 x annualized 3-month return, take the top 8. Weights: inverse volatility, capped at 20% per stock. Rebalanced monthly.",
"allocations": [
{ "symbol": "NVDA", "weightPct": 20 },
{ "symbol": "AAPL", "weightPct": 14 },
{ "symbol": "MSFT", "weightPct": 13 },
{ "symbol": "AVGO", "weightPct": 12 },
{ "symbol": "META", "weightPct": 12 },
{ "symbol": "GOOGL", "weightPct": 10 },
{ "symbol": "AMZN", "weightPct": 10 },
{ "symbol": "JPM", "weightPct": 9 }
],
"backtest": {
"period": "6M",
"methodology": "walk-forward",
"rebalance": "monthly",
"portfolio": {
"returnPct": 11.87,
"volPct": 19.62,
"maxDrawdownPct": 9.44,
"sharpe": 1.05,
"curve": [100, 102.4, 104.9, 103.2, 108.1, 111.87]
},
"benchmark": {
"symbol": "SPY",
"returnPct": 5.11,
"volPct": 12.44,
"maxDrawdownPct": 6.83,
"sharpe": 0.86,
"curve": [100, 100.8, 102.1, 101.4, 103.9, 105.11]
}
}
}
]
}
```
Backtests are history, not promises Each backtest is walk-forward: at every monthly rebalance the rule re-runs using only the data it would have had at that moment, then holds out-of-sample until the next rebalance, measured against buy-and-hold SPY over the same window. It shows what the rule did on past prices. It is not a forecast, and nothing here is investment advice. See [Backtests are history, not promises](/safety/backtests-are-history-not-promises/) and [Before you invest](/start/before-you-invest/).
# Get the theme baskets
> One deterministic basket per investing theme, with weights and a walk-forward backtest.
`GET /api/themes` returns Monvera’s open theme book: one rule-based basket per investing theme (Artificial Intelligence, Semiconductors, Quantum, Space, Crypto-linked, Cloud and Software, Energy, Fintech), computed from real 12-month price data by the same engine as the [open strategies](/dev/api/strategies/). No AI is in this loop, so the same data always produces the same weights and anyone can reproduce them. This is the raw JSON behind [monvera.best/themes](https://monvera.best/themes). > `GET /api/themes` · public, no token required · 120 requests per 60 seconds per IP. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). ## Parameters None. ## Response `{ asOf, refreshedEvery, note, themes }`. | Field | Type | Description | | ---------------- | --------- | ------------------------------------------------------------------------------------------------ | | `asOf` | string | ISO timestamp of the build. | | `refreshedEvery` | string | `"6h"`. Baskets are rebuilt from fresh market data every six hours. | | `note` | string | The methodology statement, including that backtests are walk-forward and history, not a promise. | | `themes` | object\[] | The theme list (below). | Each theme: | Field | Type | Description | | ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `slug` | string | Stable id, for example `ai` or `semiconductors`. The human page lives at `/themes/{slug}`. | | `name` | string | Display name, for example `Artificial Intelligence`. | | `tagline` | string | One-line summary. | | `method` | string | The full rule in plain words, enough to reproduce the weights. | | `excluded` | object\[] | `{ symbol, reason }` for curated names the book had to leave out (for example, no 12-month public history yet). Exclusions are named, never synthesized. | | `allocations` | object\[] | `{ symbol, weightPct }` per holding; integer weights summing to 100, inverse-volatility with a 25% per-name cap. | | `backtest` | object \| null | The walk-forward backtest, or `null` if a data gap prevented one. | Each `backtest` has the same shape as the [strategies route](/dev/api/strategies/): `period` (`"6M"`), `methodology` (`"walk-forward"`), `rebalance` (`"monthly"`), `portfolio`, and `benchmark` (`symbol: "SPY"`), each series carrying `returnPct`, `volPct`, `maxDrawdownPct`, `sharpe`, and a downsampled `curve` normalized to start at 100. ## Example
```bash
curl "https://monvera.best/api/themes"
```
```javascript
const { themes } = await (await fetch("https://monvera.best/api/themes")).json();
const ai = themes.find((t) => t.slug === "ai");
// ai.allocations -> [{ symbol: "MSFT", weightPct: 9 }, ...]
// ai.backtest.portfolio.returnPct vs ai.backtest.benchmark.returnPct
```
# Get the tradable universe
> Which assets have a live buy AND sell route right now — the two-way rule behind locked names.
`GET /api/tradability` returns the two-way tradable universe: the symbols with a live buy **and** sell route at the venues. Monvera only offers assets it can also exit — names missing either direction show in the app as locked (**Soon**), Vera declines them in both directions, and they unlock automatically when liquidity returns. > `GET /api/tradability` · public, no token required · 120 requests per 60 seconds per IP, cached 5 minutes at the edge. Base URL, response envelope, and error shapes: [conventions](/dev/conventions/). The data comes from an hourly venue sweep that probes a $15 buy of every asset and then selling that exact position back. A name locks only after **two consecutive** missed sweeps (venue books flap; one bad hour must not flap the UI) and unlocks the moment any sweep finds a route. ## Response | Field | Type | Description | | --------- | ----------------- | --------------------------------------------------------------------------------------------------- | | `ok` | string\[] \| null | Two-way tradable symbols. `null` means no sweep has landed yet — treat as unknown and lock nothing. | | `dropped` | string\[] | Currently locked symbols (two consecutive missed sweeps). | | `asOf` | string \| null | When the sweep ran. | ## Example
```bash
curl -s https://monvera.best/api/tradability | jq '{tradable: (.ok | length), locked: .dropped}'
```
# Read an account's token transfers
> Incoming and outgoing token transfers for any address, newest first, up to 50.
Returns the incoming and outgoing token transfers for any account address, newest first, deduped, capped at 50. Transfers carry Monvera’s catalog symbols (USDG, AAPL, NVDA) when the token is known. Direction is relative to the queried account: `"out"` means it sent, `"in"` means it received. `GET /api/transactions` · public, no token required. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). ## Parameters | Name | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------------------------------------- | | `address` | address | yes | The account whose transfers to read. Missing or invalid returns 400. | ## Returns | Field | Type | Description | | ----------------------------- | ------------------- | ---------------------------------------------------- | | `transactions` | array | Transfers, newest first, up to 50. | | `transactions[].hash` | bytes32 | Transaction hash. | | `transactions[].direction` | string | `"in"` or `"out"`, relative to the queried account. | | `transactions[].symbol` | string | Catalog symbol when known, else the on-chain symbol. | | `transactions[].amount` | number | Human amount moved. | | `transactions[].counterparty` | string | Recipient if `out`, sender if `in`. | | `transactions[].tokenAddress` | string | Token contract address. | | `transactions[].blockNumber` | number | Block the transfer landed in. | | `transactions[].timestamp` | number \| undefined | Unix seconds, when resolvable. | | `source` | string | `"alchemy"` or `"blockscout"`. | ## Example
```bash
curl "https://monvera.best/api/transactions?address=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
```json
{
"transactions": [
{
"hash": "0x5a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff0",
"direction": "in",
"symbol": "AAPL",
"amount": 0.54,
"counterparty": "0x7ff1a5ee19330c165146488a7ad8af6cb41da1df",
"tokenAddress": "0xaf3d76f1834a1d425780943c99ea8a608f8a93f9",
"blockNumber": 2431902,
"timestamp": 1751985127
}
],
"source": "blockscout"
}
```
## Errors | Status | Body | When | | ------ | ------------------------------------------------- | ----------------------------------------- | | 400 | `{"error":"A valid wallet address is required."}` | `address` missing or not a valid address. | ## Notes `source` names which indexer served the response: Alchemy when `ALCHEMY_API_KEY` is configured, otherwise the keyless Blockscout fallback. Both read the same on-chain transfers. An account with no transfers — or an upstream that returns nothing — yields an empty `transactions` array with `source` still set.
# Vera's on-chain track record
> Vera's recorded recommendations, executed USDG volume, recent plans, and reputation.
`GET /api/vera-record` returns Vera’s verifiable track record straight from the on-chain event log: how many recommendations she has committed, how much USDG settled against them, her most recent plans, and her IdentityRegistry reputation score. `GET /api/vera-record` · public, no token · 30 requests per 60 seconds per IP. Base URL and error shapes: [conventions](/dev/conventions/). The `record` object is aggregated from Vera’s `RecommendationCommitted` and `AllocationExecuted` logs on chain 4663; `reputation` is read live from the IdentityRegistry. Each recorded recommendation is the risk assessment Vera signed with her `agentSigner` key — the user separately signed the spend. To recover the signer of a `RiskInference` payload and confirm it equals `agentSigner()`, follow [Verify Vera](/dev/verify-vera/). ## Parameters | Name | Type | Required | Default | Description | | ------ | ------- | -------- | ------- | ----------------------------------- | | `user` | address | no | global | Restrict the record to one account. | ## Returns | Field | Type | Description | | ------------------------------ | ------------------- | ----------------------------------------------------------------------------------- | | `record.totalRecommendations` | number | Count of `RecommendationCommitted` logs. | | `record.totalExecutedUsd` | number | USDG settled across executed plans (6-decimal amounts as a number). | | `record.executedCount` | number | Count of `AllocationExecuted` logs. | | `record.recentRecommendations` | array | Up to 8 plans, newest first. | | `…[].planId` | bytes32 | The single-use plan id. | | `…[].riskScore` | number | Assessed risk in basis points. | | `…[].usdcSpent` | number \| undefined | USDG settled if the plan also executed, else absent. | | `…[].txHash` | bytes32 | Transaction that recorded the plan. | | `…[].blockNumber` | number | Block the record landed in. | | `reputation` | string \| null | `reputationScore(agentId)` as a decimal string, or null if the registry read fails. | ## Example
```bash
curl "https://monvera.best/api/vera-record?user=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
```
```json
{
"record": {
"totalRecommendations": 3,
"totalExecutedUsd": 250,
"executedCount": 3,
"recentRecommendations": [
{
"planId": "0x9f2c1d7a4b8e6c0f3a51d2e9b7c4a806f1e2d3c4b5a6978089aabbccddeeff00",
"riskScore": 4200,
"usdcSpent": 100,
"txHash": "0x5a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff0",
"blockNumber": 2431902
}
]
},
"reputation": "0"
}
```
Open any `txHash` at `https://robinhoodchain.blockscout.com/tx/` to read the `RecommendationCommitted` log yourself. ## Errors and edge cases | Status | Body | When | | ------ | ------------------------------------------- | ------------------------------ | | 400 | `{"error":"user must be a valid address."}` | `user` is not a valid address. | An account or agent with no history returns a zero-state (`totalRecommendations: 0`, empty `recentRecommendations`), not a 404. `reputationScore` is owner-updated through feedback only, so it reads `"0"` until feedback exists. A `"0"` or `null` reputation is honest, not an error — do not read it as a live performance score.
# Read and toggle saved tickers
> Read and toggle the signed-in account's saved tickers, synced across devices.
`GET` returns the signed-in account’s saved symbols, oldest first (insertion order). `POST` toggles one ticker on or off and returns the full updated list. Both directions are idempotent, so re-adding a symbol you already hold is a no-op. This list is the cross-device source of truth; the app keeps a localStorage cache for instant UX, then reconciles against this route. `GET`, `POST /api/watchlist` · requires a Privy bearer token, `401` without one. Base URL, rate limits, and error shapes: [conventions](/dev/conventions/). ## Request body (POST) | Field | Type | Required | Description | | -------- | ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `symbol` | string (1 to 12 chars) | yes | A ticker from the 95-asset catalog. Must match a symbol from [`GET /api/market`](/dev/api/market/), for example `AAPL`, `SPY`, `SGOV`. | | `on` | boolean | yes | `true` adds, `false` removes. | ## Returns | Field | Type | Description | | --------- | --------- | -------------------------------------------------------------- | | `symbols` | string\[] | Your watched tickers, oldest first. Capped at 300 per account. | ## Example
```bash
# read
curl https://monvera.best/api/watchlist \
-H "Authorization: Bearer $MONVERA_TOKEN"
# add SPY
curl -X POST https://monvera.best/api/watchlist \
-H "Authorization: Bearer $MONVERA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"symbol":"SPY","on":true}'
```
```json
{ "symbols": ["AAPL", "NVDA", "SPY"] }
```
## Errors | Status | Body | When | | ------ | --------------------------------------- | --------------------------------------------------------------------------------------- | | 400 | `{"error":"Invalid watchlist change."}` | Body fails validation: missing `symbol` or `on`, wrong type, or `symbol` over 12 chars. | | 400 | `{"error":"Unknown symbol."}` | `symbol` is not one of the 95 catalog assets. | ## Notes The rate limit applies to `POST` only, counted per account; `GET` is unmetered. See also [alerts](/dev/api/alerts/) and [notifications](/dev/api/notifications/).
# Authenticate with a Privy bearer token
> Gated routes take Authorization: Bearer plus a Privy token; public reads take no header.
Gated routes take `Authorization: Bearer `. Public routes take no header at all. The token is a Privy access token from a signed-in Monvera session, checked server-side by `verifyRequest()`. Missing, malformed, or expired, it returns `401 {"error":"Please sign in to continue."}`. There is no API key, no client secret, and no way to mint a token outside a real signed-in account. Every gated route acts for the user that token belongs to. ## Get a token Sign in at `https://monvera.best` with email or a social login, then read the access token from the Privy client SDK:
```js
import { usePrivy } from "@privy-io/react-auth";
const { getAccessToken } = usePrivy();
const privyToken = await getAccessToken();
```
Send it on every gated request:
```bash
curl -X POST "https://monvera.best/api/allocate" \
-H "Authorization: Bearer $PRIVY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"goal":"grow my money steadily for five years","amountUsdg":"250"}'
```
Tokens expire. Refresh through the SDK and resend rather than caching one for a long-running job. ## Public routes No `Authorization` header. Rate limited per client IP. | Route | Method | Notes | | ------------------------------ | ------ | ------------------------------------------------ | | `/api/market` | GET | Catalog of 95 assets; `?symbol=` and `?range=` | | `/api/prices` | GET | Current and historical prices | | `/api/screener` | GET | The catalog ranked by four price-history metrics | | `/api/strategies` | GET | Open rule-based strategies with backtests | | `/api/themes` | GET | One deterministic basket per investing theme | | `/api/backtest` | POST | Walk-forward backtest for a strategy rule | | `/api/portfolio` | GET | Positions and value; read by `?address=` | | `/api/transactions` | GET | Settled on-chain trades; read by `?address=` | | `/api/activity` | GET | Activity feed; read by `?address=` | | `/api/vera-record` | GET | Vera’s signed on-chain record; read by `?user=` | | `/.well-known/agent-card.json` | GET | Vera’s discovery document | `portfolio`, `transactions`, and `activity` are public reads keyed by `?address=`, so any account address can be inspected without a token — the chain is public either way. `backtest` is a public POST. ## Token-gated routes Require `Authorization: Bearer `. Rate limited per user id. | Route | Method | Notes | | --------------------- | ------------------- | ----------------------------------------------------- | | `/api/quote` | POST | Firm router quote; taker in the body | | `/api/allocate` | POST | Goal plus amount into a named draft plan | | `/api/commit-plan` | POST | Executes the plan and records the risk assessment | | `/api/pimlico` | POST | Gas-sponsorship relay for the ERC-4337 user operation | | `/api/watchlist` | GET / POST | The account’s saved assets | | `/api/alerts` | GET / POST / DELETE | The account’s price alerts | | `/api/notifications` | GET / POST | The account’s notification feed | | `/api/autopilot` | GET / POST / DELETE | Create, read, and revoke Autopilot | | `/api/autopilot/run` | POST | One Autopilot run within existing bounds | | `/api/autopilot/runs` | GET | History of Autopilot runs | ## What a token cannot do A bearer token authenticates a caller; it does not authorize a spend. The account is a non-custodial Privy embedded wallet the user owns, and moving funds needs their signature on the user operation and token permits. A stolen token can read an account and draft plans. It cannot buy, sell, or withdraw on its own, and no response on any route ever returns a private key, a seed phrase, or a session-signer secret. ## Routes you cannot call `/api/cron/autopilot` and `/api/cron/alerts` are gated by an internal cron secret and invoked by Cloudflare Cron. A Privy token will not reach them. Do not build against them. Requests from restricted regions are blocked in middleware before any account exists, and gated API paths return `451`. The exact body and the affected paths are in [conventions](/dev/conventions/); the policy itself is in [before you invest](/start/before-you-invest/).
# Conventions, limits, and errors
> Base URL, JSON bodies, 60-second rate limits, and every error body the API returns.
The base URL is `https://monvera.best`. Every request and response body is JSON. Every route enforces a fixed 60-second rate-limit window — public routes counted per client IP, gated routes per user id. Every error is one shape, `{"error":""}`, with the meaning carried by the status code. This page is the canonical version of all four; the route pages link here instead of repeating them. ## Requests and responses Send `Content-Type: application/json` on any route with a body and read `application/json` back. POST arguments are JSON fields, for example `{"goal":"...","amountUsdg":"250"}` on `/api/allocate`. | Value | Shape | | --------------------- | ----------------------------------------------------------------------------- | | USDG amounts | Strings of 6-decimal units — USDG has 6 decimals | | Stock and ETF amounts | 18-decimal ERC-20 units on chain 4663 | | Addresses | `0x`-prefixed hex, checksummed on the way out, case-insensitive on the way in | | `planId` | `bytes32` hex string | | Timestamps | Unix seconds | | Errors | `{"error":""}` plus a status code | Match on the status code, not the message text — some routes pass a more specific 400 message inside the same shape. Error messages never expose internals. Danger No route returns a private key, a seed phrase, or a session-signer secret in any field. Accounts are non-custodial and signing happens client-side through Privy. If an example shows a key in a response, it is fabricated. ## Error codes | Status | Body | Triggered by | | ------ | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | 400 | `{"error":"Invalid request."}` | A malformed or missing field. Some routes return a more specific message in the same shape. | | 401 | `{"error":"Please sign in to continue."}` | A gated route with no bearer token, or a bad or expired Privy token. | | 429 | `{"error":"Too many requests. Please slow down a moment."}` | The route’s 60-second window is exceeded. Carries a `Retry-After` header in seconds. | | 500 | `{"error":"Something went wrong. Please try again."}` | An unexpected server error. Retry; if it persists, say so on X at [@monvera\_best](https://x.com/monvera_best). | On `429`, read `Retry-After` and wait that many seconds. `/api/autopilot/run` also holds an in-flight lock and returns `429` with `Retry-After: 30` when a run is already executing, even under the request limit. On `400`, fix the body and resend — `/api/alerts` POST returns `400` once an account already holds 20 or more active alerts, independent of the rate limit. There is no geo-gating: Monvera is available worldwide, and no route returns a region-based `451`. (The former US/CA/UK/CH gate was removed in July 2026.) ## Rate limits, public routes (per IP) | Route | Limit per 60s | | ------------------- | ------------- | | `/api/prices` | 60 | | `/api/market` | 60 | | `/api/screener` | 60 | | `/api/vera-record` | 30 | | `/api/portfolio` | 240 | | `/api/transactions` | 120 | | `/api/activity` | 120 | | `/api/strategies` | 30 | | `/api/backtest` | 20 | ## Rate limits, gated routes (per user id) | Route | Limit per 60s | | ----------------------- | ------------- | | `/api/pimlico` | 600 | | `/api/watchlist` (POST) | 60 | | `/api/quote` | 30 | | `/api/commit-plan` | 20 | | `/api/autopilot` (POST) | 20 | | `/api/alerts` (POST) | 20 | | `/api/allocate` | 12 | | `/api/autopilot/run` | 6 | ## On-chain reverts `commit-plan` can accept your request and still revert on-chain. The revert rolls the whole transaction back, so a failed commit spends nothing. In order: `PlanAlreadyRecorded(planId)` if that `planId` was already committed, `InferenceExpired()` if the block timestamp is past `expiry`, `RiskCeilingBreached(assessed, maxRisk)` if `assessedRisk` exceeds `maxRisk`, and `BadSigner(recovered)` if the recovered signer is not the current `agentSigner()`. An expired inference is the common one, and a stale quote is usually the cause. Allocate and commit close together, and quote each leg as you execute it. ## The worked example Request examples across these pages use one story, so field values line up between routes: a plan named “Steady Growth” over Apple (AAPL), Nvidia (NVDA), the S\&P 500 (SPY), and US Treasuries (SGOV), funded in USDG on chain 4663. | Symbol | Token address (chain 4663) | | ------ | -------------------------------------------- | | AAPL | `0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9` | | NVDA | `0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC` | | SPY | `0x117cc2133c37B721F49dE2A7a74833232B3B4C0C` | | SGOV | `0x92FD66527192E3e61d4DDd13322Aa222DE86F9B5` | `GET /api/market` is the source of truth for all 95 addresses — take them from the catalog, not from this table, if you are building against it.
# MCP server
> Connect an AI agent to these docs over MCP at docs.monvera.best/mcp — search and read every page.
These docs are an MCP server. Point any Model Context Protocol client at `https://docs.monvera.best/mcp` and it can search and read every page — no key, no signup, no rate limit.
```plaintext
https://docs.monvera.best/mcp
```
It speaks Streamable HTTP over a single `POST`. There is no authentication because there is nothing to protect: the corpus is the same public documentation you are reading, and the server is strictly read-only. ## Tools | Tool | What it does | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `search_monvera_docs` | Find pages by keyword. Takes `query`, optional `section` (`start`, `use`, `safety`, `how`, `dev`, `reference`) and `limit`. Returns each match with its id, title, URL, and an excerpt. | | `get_monvera_doc` | Return one page in full. Takes `id`, the value `search_monvera_docs` gave you — for example `use/your-plan`. A full URL works too. | The intended loop is search first, then read: `search_monvera_docs` to find the page, `get_monvera_doc` to get its text. ## Connect a client Most clients take a URL. In an `mcpServers` config block:
```json
{
"mcpServers": {
"monvera-docs": {
"type": "http",
"url": "https://docs.monvera.best/mcp"
}
}
}
```
With the Claude Code CLI:
```bash
claude mcp add --transport http monvera-docs https://docs.monvera.best/mcp
```
## Call it directly Every MCP message is JSON-RPC, so `curl` is a fine client:
```bash
curl -s -X POST https://docs.monvera.best/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search_monvera_docs",
"arguments":{"query":"what it costs","limit":3}}}'
```
## Plain text, if you prefer Not every agent speaks MCP. The same content is published as plain text: | File | What it holds | | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | [`/llms.txt`](https://docs.monvera.best/llms.txt) | Every page, grouped, with a one-line description each. Start here. | | [`/_llms-txt/using-monvera.txt`](https://docs.monvera.best/_llms-txt/using-monvera.txt) | The full text of every product page. | | [`/_llms-txt/developer-and-api.txt`](https://docs.monvera.best/_llms-txt/developer-and-api.txt) | The full text of every developer and API page. | | [`/llms-full.txt`](https://docs.monvera.best/llms-full.txt) | The whole site in one file. | Vera reads `/llms.txt` herself, which is why she can point you at a page when you ask her something these docs answer. Caution The server answers questions about Monvera’s documentation. It does not place trades, read your account, or touch your money — those live behind the [authenticated API](/dev/authentication/), which needs your own token.
# Network and addresses
> Chain 4663 values, RPC, explorer, and every Monvera contract address, Blockscout-linked.
Every chain value and contract address Monvera uses is on this page, each linked to Blockscout. Take addresses from here or from `GET /api/market` — never from a tweet, a screenshot, or a search result. ## Network values Monvera settles on Robinhood Chain, an Arbitrum-stack Ethereum L2. The native gas token is ETH (18 decimals), though accounts never hold it: gas is sponsored through ERC-4337. | Value | Mainnet | Testnet | | --------------------- | ----------------------------------------- | ---------------------------------------------- | | Chain id | `4663` | `46630` | | RPC | `https://rpc.mainnet.chain.robinhood.com` | `https://rpc.testnet.chain.robinhood.com` | | Explorer (Blockscout) | `https://robinhoodchain.blockscout.com` | `https://explorer.testnet.chain.robinhood.com` | ## Contracts on chain 4663 | Contract | Address | What it backs | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | VeraRecord | [`0x7ff1a5ee19330c165146488a7ad8af6cb41da1df`](https://robinhoodchain.blockscout.com/address/0x7ff1a5ee19330c165146488a7ad8af6cb41da1df) | `record(...)`, `agentSigner()`, `committed(planId)`; emits `RecommendationCommitted` and `AllocationExecuted`; EIP-712 domain `VeraRecord` | | GroveManager | [`0x5ee5df3e027fd5d9a3fbba4cb19b0edeb4b33fb8`](https://robinhoodchain.blockscout.com/address/0x5ee5df3e027fd5d9a3fbba4cb19b0edeb4b33fb8) | Buys, exits and manages [Groves](/use/groves/) into your own wallet; `positionOf`, `groveComposition`, `buy`, `exit`; emits `Bought`, `Exited`, `Rebalanced`. `feeBps` and `treasury` are immutable, and no function can move a user’s holdings | | IdentityRegistry | [`0x751ae640cfa816404b017fbb8234dd21abafbbdc`](https://robinhoodchain.blockscout.com/address/0x751ae640cfa816404b017fbb8234dd21abafbbdc) | ERC-8004 ERC-721 agent identity; Vera is agent #1; `reputationScore(agentId)`, `tokenURI(agentId)` | | USDG | [`0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`](https://robinhoodchain.blockscout.com/address/0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168) | Settlement currency, 6 decimals; every trade settles in USDG | | WETH | [`0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73`](https://robinhoodchain.blockscout.com/address/0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73) | Intermediate token (18 decimals) for router paths | | Multicall3 | [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://robinhoodchain.blockscout.com/address/0xcA11bde05977b3631167028862bE2a173976CA11) | Batched view reads, at the canonical Multicall3 address | | Permit2 | [`0x000000000022D473030F116dDEE9F6B43aC78BA3`](https://robinhoodchain.blockscout.com/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | Token approvals for venue settlement | `VeraRecord` and the `InferenceVerifier` verify path are the same contract at `0x7ff1a5ee19330c165146488a7ad8af6cb41da1df`. ## Asset tokens The 95 tradable stocks and ETFs are 18-decimal ERC-20 tokens in the ERC-8056 family. Their addresses are not listed here on purpose: the catalog changes, so pull them from [`/api/market`](/dev/api/market/), which is the source of truth. | Symbol | Address | | ------ | -------------------------------------------- | | AAPL | `0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9` | | NVDA | `0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC` | | SPY | `0x117cc2133c37B721F49dE2A7a74833232B3B4C0C` | | SGOV | `0x92FD66527192E3e61d4DDd13322Aa222DE86F9B5` | Those four are the “Steady Growth” worked example used across these pages, included so the request samples can be run as written. ## Vera’s identity on Base (chain 8453) Vera’s canonical ERC-8004 identity lives on Base, not on 4663. The registry is [`0x8004a169fb4a3325136eb29fa0ceb6d2e539a432`](https://basescan.org/address/0x8004a169fb4a3325136eb29fa0ceb6d2e539a432), agent id `58228`, registered via Virtuals ACP, and declared in `registrations[0]` of her agent card. The 4663 IdentityRegistry above is Monvera’s own registry and holds Vera as agent #1; it is not the canonical one. ## The trusted signer `agentSigner()` on VeraRecord was last read as `0xe532105523d4eD559c3a53E3E82D616bE1a085c5`. This key can rotate. Read it live before asserting against a recovered signature rather than hardcoding it — the recipe is on [verify Vera](/dev/verify-vera/). ## $MONVERA The $MONVERA token is at [`0x7541872e32Bb529d7FF11D6C59832269ce33a6FF`](https://robinhoodchain.blockscout.com/token/0x7541872e32Bb529d7FF11D6C59832269ce33a6FF), on Robinhood Chain, not on Base. It is unrelated to the trading stack: no route above touches it, and it is not an investment product. ## $MONVERA staking Staking is a separate, self-contained system. `MonveraStaking` holds staked $MONVERA and has no owner, no pause, and no upgrade path — nobody, including the team, can move, freeze, or redirect a stake, and the unstake cooldown is fixed at deploy. See the [staking guide](/use/staking/) for how it works and what a season pays. | Contract | Address | What it backs | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | MonveraStaking | [`0xd6b6c5587499fea30d5c5147ebec1f6043c27f2a`](https://robinhoodchain.blockscout.com/address/0xd6b6c5587499fea30d5c5147ebec1f6043c27f2a) | Stake $MONVERA. Ownerless, no admin, immutable 14-day unstake cooldown. Accrues the stake-time weight a season is split by; `stakedOf`, `pendingOf`, `weightOf`, `totalWeight` | | SeasonDistributor | [`0xe51658ee2fed7ae09b81a91e2e0ffc6698648163`](https://robinhoodchain.blockscout.com/address/0xe51658ee2fed7ae09b81a91e2e0ffc6698648163) | Merkle claims for a closed season. Per-season funded and accounted; `claim(...)` pays exactly the leaf amount against a published root, once | | GroveCuratorRegistry | [`0xe1882878df4e39566abea9ef9200d73dba83a0cf`](https://robinhoodchain.blockscout.com/address/0xe1882878df4e39566abea9ef9200d73dba83a0cf) | The on-chain record of which staker curates each Grove and the fee share owed, capped at 50%; `curatorEligible`, `curatorOf` | All three are verified on Blockscout. The registry and distributor are owned by a cold key; the staking contract itself is ownerless.
# Your first Monvera API call
> Two public GETs need no token; two authenticated POSTs build and commit a plan.
Run this now. No account, no key, no header:
```bash
curl "https://monvera.best/api/market"
```
That returns the catalog of 95 real tokenized stocks and ETFs with live prices — the same numbers the app shows. Reads are public. Anything that builds a plan or spends money needs a Privy bearer token, and this page walks both halves in four calls. ## Public reads, no token `GET /api/market` is the source of truth for symbols, names, live prices, and token addresses on chain 4663. Pass `symbol` for one asset and `range` for a price window:
```bash
curl "https://monvera.best/api/market?symbol=AAPL&range=1y"
```
`GET /api/screener` ranks that same catalog by four figures computed from real price history — one-year return (`ret1yPct`), three-month return (`ret3mPct`), volatility (`volPct`), and maximum drawdown (`maxDrawdownPct`). Assets with no history are flagged, not scored:
```bash
curl "https://monvera.best/api/screener"
```
Sort and filter the rows client-side. A ranking you compute from this response matches what a user sees in the app. ## What the API is underneath Monvera is a conversation. Nobody in the app calls an endpoint — they say what they want, and Vera’s router resolves it into one of 26 intents and opens a panel beside the chat: You Put $250 into something steady for the long run Vera Here’s a plan — Apple, Nvidia, the S\&P 500, and US Treasuries, about $62 each. Nothing moves until you confirm. `POST /api/allocate` is that same step over HTTP. The rest of this page uses that plan, “Steady Growth”, as its worked example. ## Get a token Sign in at `https://monvera.best`, then read the Privy access token from the client SDK and export it:
```bash
export PRIVY_TOKEN=""
```
[Authentication](/dev/authentication/) covers where the token comes from and which routes require it. ## Turn a goal into a draft plan `POST /api/allocate` takes a plain-language goal and a USDG amount and returns a named draft: legs with an asset, a weight, a one-line reason, and a plain risk read, plus a `planId`, an `assessedRisk`, and a `maxRisk` ceiling.
```bash
curl -X POST "https://monvera.best/api/allocate" \
-H "Authorization: Bearer $PRIVY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"goal":"grow my money steadily for five years","amountUsdg":"250"}'
```
Nothing is signed or spent at this point. You can start from $1 and no venue enforces a minimum, though each leg is a separately sponsored transaction, so very small slices are inefficient. Amounts are strings of 6-decimal USDG units. ## Commit it `POST /api/commit-plan` executes the plan and records Vera’s signed EIP-712 risk assessment on-chain in the same transaction that buys the stocks — one atomic record anyone can verify on the explorer. Gas is sponsored through ERC-4337 — the account never holds a gas token.
```bash
curl -X POST "https://monvera.best/api/commit-plan" \
-H "Authorization: Bearer $PRIVY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"planId":"0x9f2c...a41b","address":"0xYourSmartAccountAddress"}'
```
You get a transaction hash back. Open it on `https://robinhoodchain.blockscout.com` and you will find a `RecommendationCommitted` log and an `AllocationExecuted` log emitted by `0x7ff1a5ee19330c165146488a7ad8af6cb41da1df`. That is the checkable record; [verify Vera](/dev/verify-vera/) reproduces the signature from it yourself. Allocate and commit close together. The on-chain verify path reverts the whole transaction if the assessment has expired, if `assessedRisk` exceeds `maxRisk`, if the recovered signer is not the current `agentSigner()`, or if the same `planId` was already recorded — a failed commit spends nothing. ## What settlement looks like Every leg settles atomically in its own sponsored transaction — the position is held and sellable immediately. It is already bought and already the holder’s, and it counts in the balance the whole time — it just cannot be sold until it lands. Nothing is stuck. There is no route that quotes a whole plan at once. Firm per-leg quotes come from [`POST /api/quote`](/dev/api/quote/), one leg at a time as you execute it — quote the whole plan upfront and the tail legs go stale before they settle. Every route, request body, and response shape is in the [API reference](/dev/api/). Nothing here is investment advice, and backtests are history, not promises.
# Vera on OKX.AI
> Vera is agent #6711 on OKX.AI: pay-per-call stock research over xStocks, paid via x402.
Vera is listed on [OKX.AI](https://www.okx.ai), OKX’s agent marketplace, as **agent #6711, “Vera by Monvera”**. Any person or agent can pay her per call for research, allocation plans, themed baskets, and ready-to-execute trade instructions over real tokenized stocks (xStocks). This listing runs on its own infrastructure and is separate from the Monvera app on Robinhood Chain. ## What she sells All services live at `https://vera.monvera.best`, priced in USDT per call: | Service | Price | What you get | | ----------------------------- | ----- | ---------------------------------------------------------------------------------- | | Live Tokenized Stock Quote | free | Live price and liquidity snapshot for one xStock | | AI Stock Allocation Plan | $0.15 | Goal, budget, and risk into a weighted basket with reasons and a 1-year backtest | | Themed Stock Basket | $0.08 | halal · blue-chip · index · ai-semis · dividend · momentum · barbell | | Portfolio Rebalance Planner | $0.08 | Holdings plus target into minimal sell/buy instructions | | Stock Head-to-Head Compare | $0.05 | Two stocks, real 12-month data, a clear verdict | | Tokenized Stock Research Note | $0.03 | One stock, the 12-month picture, the key risk, who it fits | | Halal Stock Compliance Screen | $0.03 | AAOIFI sector and ratio screens with a reason per name (certification in progress) | | Portfolio Backtest | $0.03 | Any weighted basket against SPY: return, volatility, worst dip, Sharpe | | Momentum Stock Screener | $0.03 | The whole universe ranked by risk-adjusted momentum | | Trade Legs Builder | $0.03 | A plan turned into ordered, executable swap instructions | The universe is 37 tokenized stocks and ETFs (xStocks by Backed, on Solana), each verified tradable with a real swap-build before it is allowed into a plan. Names that quote but cannot execute are excluded until liquidity appears. ## For agents: the machine-readable surface Everything an agent needs is self-serve — no human required: [`/llms.txt`](https://vera.monvera.best/llms.txt) (the complete runbook: payment flow, signing schema, transport rules, execution steps, verification), [`/openapi.json`](https://vera.monvera.best/openapi.json), [`/.well-known/x402.json`](https://vera.monvera.best/.well-known/x402.json), plus free helpers: `GET /v1/preflight` (funding check before you pay), `GET /v1/record/{planId}` (on-chain commitment check) and `POST /v1/build/stage` (stage an external plan for query-string use). Every error carries a corrected, copy-pasteable retry hint. ## Fees & costs The per-call price is the only fee Vera charges — no execution fees, no spread markup, no percentage of your order. All-in cost examples and the slippage-inclusive worst case are published live at `GET https://vera.monvera.best/` under `costs`, and every pricing change is dated under `pricing.changelog`. ## Refunds If you were charged and received a 5xx, retry the identical request with the same `PAYMENT-SIGNATURE` — settlement is skipped and the result regenerated free (your payment is recorded server-side before the engine runs). If retries still fail after 24 hours, contact us here with the `paymentId` from the error body: we verify it against our payment ledger and refund the full call price in USDT0 on X Layer to the paying address within 72 hours. Payments are otherwise final; there are no protocol-level refunds. ## How paying works Every paid endpoint speaks x402. An unpaid call returns HTTP 402 with a payment challenge, your wallet signs a gasless USDT authorization, the OKX broker settles it on X Layer, and the same call retried with the payment header returns the result. From OKX.AI or any onchainos-enabled agent this is automatic — you see a price confirmation, nothing more. Payments settle in USDT0 or USDG on X Layer (chain 196). Execution is separate: the legs Vera returns are swaps on Solana, where xStocks liquidity actually lives, run by your own wallet through the OKX DEX aggregator. Buys and sells both work, at any amount. ## Her record is on-chain here too Every paid plan or basket is committed to **VeraRecordV2** on X Layer at [`0xd97cd1a25484252bf234ab384c3818b05e6594e0`](https://www.oklink.com/x-layer/address/0xd97cd1a25484252bf234ab384c3818b05e6594e0). Each `RecommendationCommitted` event binds the plan hash, the payer, the amount paid, the risk score, and the leg count under one EIP-712 signature from her agent key, so entries cannot be forged and anyone can audit what Vera recommended, to whom, and when. The same recover-the-signer method on [verify Vera](/dev/verify-vera/) works against this contract. Backtests are history, not promises. The record makes the history checkable. ## Relationship to the Monvera app Same Vera, different venue. The [Monvera app](https://monvera.best) is her home: real tokenized stocks on Robinhood Chain, bought by talking to her, gasless and non-custodial. The OKX.AI listing packages her research brain as pay-per-call services for the agent economy, with its own wallet, its own revenue, and its own on-chain record. Nothing you do on one touches the other.
# Vera on Virtuals ACP
> Vera sells her analysis on the Virtuals Agent Commerce Protocol: seven services, escrow-paid in USDG on Robinhood Chain.
Vera is a seller on the Virtuals Agent Commerce Protocol as [**“Vera by Monvera”**](https://app.virtuals.io/acp/agent/019f619c-6f1a-7768-b2c3-1b5f7b8a340d), on Robinhood Chain. Any ACP agent — or anyone driving the ACP SDK or CLI — can hire her for stock analysis over real tokenized stocks. Jobs settle in **USDG escrow on Robinhood Chain (4663)**, with gas sponsored, so hiring her costs exactly the listed price. ## What she sells | Offering | Price | SLA | Requirement | | ------------------------------- | ----- | ------ | ----------------------------------------------------------------------------------------------------------------------- | | `ai_stock_allocation_plan` | $0.15 | 10 min | `{ "goal": "steady growth", "amountUsd": 100, "riskTolerance": "moderate" }` | | `themed_stock_basket` | $0.08 | 10 min | `{ "theme": "halal", "amountUsd": 100 }` — themes: halal · blue-chip · index · ai-semis · dividend · momentum · barbell | | `stock_head_to_head_compare` | $0.05 | 5 min | `{ "symbolA": "AAPLx", "symbolB": "MSFTx" }` | | `tokenized_stock_research_note` | $0.03 | 5 min | `{ "symbol": "AAPLx" }` | | `halal_stock_compliance_screen` | $0.03 | 5 min | `{ "symbols": "AAPLx,MSFTx" }` | | `portfolio_backtest` | $0.03 | 5 min | `{ "symbols": "AAPLx,NVDAx", "weights": "60,40" }` | | `momentum_stock_screener` | $0.03 | 5 min | `{}` — no inputs needed | Offering names are the exact identifiers you hire by. Requirements are validated against the offering schema before a job is created, and again before money moves — a requirement that cannot run is rejected before funding, never after. ## How a job runs The standard ACP lifecycle: you create a job from an offering with your requirement, Vera proposes the listed price as the budget, you fund the escrow, she delivers, you (or your evaluator) approve and the escrow releases. If she cannot deliver, she rejects the job with a reason and the escrow resolves back to you — a paid job is never left hanging. Deliverables are JSON matching the offering’s deliverable schema. Allocation plans and baskets are committed on-chain via VeraRecord before delivery, so her track record is publicly checkable — commitments prove what she recommended and when, not how it performed. Backtests are history, not promises. ## Same Vera, three venues The [Monvera app](https://monvera.best) is her home: real tokenized stocks on Robinhood Chain, bought by talking to her, gasless and non-custodial. The [OKX.AI listing](/dev/vera-on-okx-ai/) sells her research pay-per-call over x402, including executable trade instructions. This ACP listing packages the same analysis for the Virtuals agent economy — escrow-paid, analysis only, with its own wallet and its own on-chain record. Nothing you do on one touches the others.
# Verify Vera yourself
> Recover the signer of any recorded plan and check it against the live agentSigner().
You can prove Vera authored a plan’s risk assessment without trusting Monvera. Read `agentSigner()` live from the `VeraRecord` contract, fetch the signed `RiskInference` payload, ECDSA-recover its signer, and assert the two match. Then confirm the same `planId` appears in an on-chain `RecommendationCommitted` log. Every value below is on-chain or on a public endpoint. This proves authorship, not performance. It says Vera’s key signed that exact risk call before the trade, nothing about whether the plan makes money. What she signs versus what the user signs is covered in [accountable AI](/how/accountable-ai/). ## Vera’s two identities | Identity | Chain | Registry | Agent id | Role | | ------------------------ | ---------------------- | -------------------------------------------- | -------- | ---------------------------------------------- | | Canonical ERC-8004 | Base (8453) | `0x8004a169fb4a3325136eb29fa0ceb6d2e539a432` | `58228` | Portable identity, registered via Virtuals ACP | | Monvera IdentityRegistry | Robinhood Chain (4663) | `0x751ae640cfa816404b017fbb8234dd21abafbbdc` | `1` | The agent id recorded with every plan on 4663 | The signature you recover on this page is checked against `agentSigner()` on `VeraRecord`, chain 4663. The Base entry is declared in `registrations[0]` of her agent card and is the one the card labels canonical; do not label the 4663 registry canonical unless the published card changes. ## The agent card `GET /.well-known/agent-card.json` is Vera’s discovery document, cached `s-maxage=3600`. It is a custom shape, not the generic A2A schema. * curl
```sh
curl https://monvera.best/.well-known/agent-card.json
```
* fetch
```js
const card = await fetch(
'https://monvera.best/.well-known/agent-card.json',
).then((r) => r.json());
```
Top-level fields, in order: | Field | Type or value | What it holds | | ---------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `name` | `"Vera"` | The agent name | | `description` | string | Human-readable summary | | `image` | `"https://monvera.best/icon-512.png"` | Avatar | | `active` | `true` | Whether the agent is live | | `persona` | `{ role, blurb }` | Role and blurb | | `services` | `{ web: { url } }` | The web app URL | | `endpoints` | `{ app, agent, strategies }` | App, agent-card, and strategies URLs | | `supportedTrust` | `["reputation"]` | The trust model Vera declares | | `skills` | 5 slugs | `portfolio-allocation`, `plain-language-allocation`, `dca-autopilot`, `trading-strategy`, `risk-management` | | `domains` | `["finance","investing","real-world-assets"]` | Subject domains | | `x402` | `false` | No x402 payment gating on this deployment | | `registrations` | array | On-chain identity registrations | | `metadata` | object | Chain, contracts, and the current signer | `metadata` carries what you need to verify a record on 4663:
```json
{
"version": "2.3",
"chain": "robinhood-chain",
"chainId": 4663,
"app": "https://monvera.best/app",
"explorer": "https://robinhoodchain.blockscout.com",
"identityRegistry": "0x751ae640cfa816404b017fbb8234dd21abafbbdc",
"agentId": 1,
"agentSigner": "0xe532105523d4eD559c3a53E3E82D616bE1a085c5"
}
```
Caution `metadata.agentSigner` is the last known signer and it can rotate. Read `agentSigner()` live from `VeraRecord` before asserting equality. Never hardcode this address as permanent. ## The struct she signs Vera signs an EIP-712 `RiskInference` typed message. The type and domain are fixed by the deployed contract:
```solidity
keccak256("RiskInference(bytes32 planId,uint16 assessedRisk,uint16 maxRisk,uint256 expiry)")
```
Fields in order: `bytes32 planId`, `uint16 assessedRisk`, `uint16 maxRisk`, `uint256 expiry`. The domain:
```json
{
"name": "VeraRecord",
"version": "1",
"chainId": 4663,
"verifyingContract": "0x7ff1a5ee19330c165146488a7ad8af6cb41da1df"
}
```
`VeraRecord.record(...)` recovers the signer of this payload on-chain and reverts if anything is wrong, so a plan cannot be recorded with a forged or stale assessment, and the same `planId` cannot be recorded twice. The record lands in the same transaction as the buys, which is what makes the track record uneditable after the fact. The revert order is in [conventions](/dev/conventions/). ## Recover the signer 1. **Read `agentSigner()` live.**
```js
import {
createPublicClient,
http,
parseAbiItem,
recoverTypedDataAddress,
} from 'viem';
const VERA_RECORD = '0x7ff1a5ee19330c165146488a7ad8af6cb41da1df';
const client = createPublicClient({
transport: http('https://rpc.mainnet.chain.robinhood.com'),
});
const agentSigner = await client.readContract({
address: VERA_RECORD,
abi: [parseAbiItem('function agentSigner() view returns (address)')],
functionName: 'agentSigner',
});
```
2. **Fetch the signed payload and signature.** `GET /api/vera-record?user=` returns each recorded plan with its `RiskInference` fields and Vera’s signature. It is a public read. * curl
```sh
ACCOUNT=0xYourAccountAddress
curl "https://monvera.best/api/vera-record?user=$ACCOUNT"
```
* fetch
```js
const res = await fetch(
`https://monvera.best/api/vera-record?user=${account}`,
).then((r) => r.json());
const record = res.records[0];
const { planId, assessedRisk, maxRisk, expiry, signature } = record;
```
3. **Confirm the plan is on-chain.** Pull `RecommendationCommitted` logs from the deploy block. Public RPC caps `eth_getLogs` at 10,000 blocks, so scan in chunks of 9,999.
```js
const event = parseAbiItem(
'event RecommendationCommitted(bytes32 indexed planId, address indexed user, bytes32 recHash, uint16 riskScore, uint256 agentId)',
);
const DEPLOY_BLOCK = 2429508n;
const STEP = 9999n;
const latest = await client.getBlockNumber();
let logs = [];
for (let from = DEPLOY_BLOCK; from <= latest; from += STEP + 1n) {
const to = from + STEP > latest ? latest : from + STEP;
logs = logs.concat(
await client.getLogs({ address: VERA_RECORD, event, fromBlock: from, toBlock: to }),
);
}
const onChain = logs.find((l) => l.args.planId === planId);
// onChain.args.riskScore equals assessedRisk; onChain.args.agentId is 1n
```
4. **Recover and assert.**
```js
const recovered = await recoverTypedDataAddress({
domain: {
name: 'VeraRecord',
version: '1',
chainId: 4663,
verifyingContract: VERA_RECORD,
},
types: {
RiskInference: [
{ name: 'planId', type: 'bytes32' },
{ name: 'assessedRisk', type: 'uint16' },
{ name: 'maxRisk', type: 'uint16' },
{ name: 'expiry', type: 'uint256' },
],
},
primaryType: 'RiskInference',
message: { planId, assessedRisk, maxRisk, expiry: BigInt(expiry) },
signature,
});
const authored = recovered.toLowerCase() === agentSigner.toLowerCase();
// true means Vera's agentSigner authored this exact risk assessment
```
When `authored` is `true` and the same `planId` appears in a `RecommendationCommitted` log, you have confirmed two things without trusting anyone: Vera’s signer authored that risk assessment, and it was recorded in the transaction that ran the trades. Browse the same records for any address on Blockscout at [`0x7ff1a5ee19330c165146488a7ad8af6cb41da1df`](https://robinhoodchain.blockscout.com/address/0x7ff1a5ee19330c165146488a7ad8af6cb41da1df). ## Reputation reads zero for now `reputationScore(agentId)` on the IdentityRegistry returns `0` until feedback is written — the score is owner-updated through feedback, not computed live. Do not read a nonzero score as proof of anything yet.
```js
const IDENTITY_REGISTRY = '0x751ae640cfa816404b017fbb8234dd21abafbbdc';
const score = await client.readContract({
address: IDENTITY_REGISTRY,
abi: [parseAbiItem('function reputationScore(uint256 agentId) view returns (uint256)')],
functionName: 'reputationScore',
args: [1n],
});
// 0n until feedback is written for agent #1
```
`/api/vera-record` returns that same live score alongside the recovered signer, the agent id, and Vera’s recent recommendations, so you do not have to reassemble history from log chunks yourself. Its full response shape is at [`/api/vera-record`](/dev/api/vera-record/).