# Vurto AI API Guide

This document is the machine-operator guide for AI agents (Clawbot/OpenClaw style) to execute Vurto actions directly via API.

> **Always use the latest version.** This guide, the MCP tool descriptions, and the
> `@vurto/sign-tx` CLI change frequently. Before each run: (1) re-fetch this page live from
> `https://ana.vurto.cc/agent-api.md` — never trust a cached/older snapshot; (2) re-read the MCP
> `tools/list` descriptions, which carry the current contract; (3) run the `@vurto/sign-tx`
> version that `prepare_signing` returns (`cli.version` / `methods.*.command`), never a hardcoded
> older one. If behavior disagrees with a cached copy, the live surface wins.

## 1. Base URLs

- Production: `https://ana.vurto.cc/api/v1`
- OpenAPI Spec: `https://ana.vurto.cc/api/v1/openapi.json`

Health check:

```bash
curl -sS https://ana.vurto.cc/api/v1/health
```

Public call template (no API key required):

```bash
curl -sS https://ana.vurto.cc/api/v1/donator-status \
  -H 'content-type: application/json' \
  --data '{"walletAddress":"0x1111111111111111111111111111111111111111","chainId":137}'
```

## 2. Agent Contract (Clawbot/OpenClaw Friendly)

For each command, the agent should always keep this contract:

1. Validate intent + required params.
2. Resolve donor status (`/donator-status`) before quote/build.
3. Quote first if command requires swap math (`/quote/*`).
4. Build tx payload (`/build/*`) and inspect `to`, `data`, `value`, `feeBps`, `feeAmount`.
5. Sign and send onchain.
6. Report status to tracking endpoint (`/transactions/:id/*`) when `transactionId` exists.
7. Return final result with tx hash, chain, token amounts, and effective fee.

## 3. Common Rules

- All token amounts are integer strings in raw units (wei-like, token decimals).
- Token object shape:

```json
{
  "address": "0x...",
  "decimals": 6,
  "symbol": "USDC"
}
```

- Minimum shared fields in most calls:
  - `chainId`
  - `walletAddress`
  - `userAddress` (required for quote/build swap routes)
- Pool routes require `poolAddress`.
- Default interest mode for borrow/repay: variable (`2`).
- Public routes (no credential): `GET /health` and docs/root guide.
- Sensitive routes are also public on `ana.vurto.cc` and `ana.vurto.cc`:
  - `POST /donator-status`
  - `POST /quote/*`
  - `POST /build/*`
- Anti-flood rate limits are layered by IP + credential + wallet + route + method.
- On limit hit, API returns `429` with headers:
  - `retry-after`
  - `x-ratelimit-limit`
  - `x-ratelimit-remaining`
  - `x-ratelimit-reset`
  - `x-ratelimit-scope`
- Extra telemetry header: `x-ratelimit-source` (`edge`, `edge_local`, or `local`).
- Agent behavior on `429`: wait `retry-after` seconds and retry with backoff; avoid parallel bursts.
- OpenAPI contract guard is active on sensitive routes: unknown/missing fields are rejected.
- Kill-switch can temporarily disable specific routes and returns `503 api_route_disabled`.

## 3.1 APY History (Self-Collected + DeFi Llama)

Vurto independently collects supply and borrow APY rates every 12 hours (00:00 and 12:00 UTC). This self-collected data is combined with DeFi Llama where available, and over time will provide full 365-day coverage without external dependencies.

Endpoints:

- `GET /apy/history?chainId=<id>` — current rates (supply + borrow) from DeFi Llama bulk
- `GET /apy/history/rolling?chainId=<id>&days=365` — rolling averages from self-collected + DeFi Llama charts

Recommended query for agents:

```bash
curl -sS "https://ana.vurto.cc/api/v1/apy/history/rolling?chainId=42161&days=365"
```

How to read the response:

- `source`: data source mode — `vurto_collected`, `defillama+vurto_collected`, or `defillama+rolling_chart`
- `coverageDays`: max coverage across supply and borrow for that token
- `supplyCoverageDays`: days of supply APY history available
- `borrowCoverageDays`: days of borrow APY history available (from self-collection)
- `supply.avg30d|avg90d|avg180d|avg365d` — supply APY rolling averages
- `borrow.avg30d|avg90d|avg180d|avg365d` — borrow APY rolling averages
- `collection.collectedDays`: number of unique daily snapshots Vurto has recorded
- `collection.notice`: human-readable status of the collection progress

Important rules for agents:

- Consider `30d/90d/180d/365d` values valid only when `coverageDays >= window` (or the specific `supplyCoverageDays`/`borrowCoverageDays`).
- If coverage is insufficient, treat that window as unavailable (`N/A`) instead of extrapolating.
- Prefer token lookup by underlying address; use `symbol:<SYMBOL>` alias only as fallback.
- Rolling endpoint is capped at `days=365`.
- Use `collection.collectedDays` to know how mature the borrow average data is.

Background collection:

- APY snapshots (both supply and borrow current rates) are persisted in Vurto KV every 12 hours.
- Borrow averages grow daily as more snapshots accumulate — goal is full 365-day independence within one year.

## Support Error Reporting for AI Agents

- Purpose: allow agents to report runtime issues and retrieve aggregated incident context for debugging loops.
- Write endpoint (public intake): `POST /support/tickets`
- Read endpoints (private): `GET /support/tickets`, `GET /support/tickets/:id`, `GET /support/analysis`
- Privacy rule: support reads require valid credential; unauthenticated reads return `401 support_read_auth_required`.

Recommended agent flow:

1. On execution failure, call `POST /support/tickets` with `message`, optional `contactEmail`, and optional `screenshotDataUrl`.
2. For triage sessions, call `GET /support/analysis` (with credential) to cluster recurring signatures.
3. If needed, fetch detailed incidents via `GET /support/tickets` and `GET /support/tickets/:id`.

## 4. Fee and Donor Policy

- Base fee: `2.5 bps` = `0.025%`.
- If wallet is verified donor: fee = `0`.
- Verify donor:

```bash
curl -sS https://ana.vurto.cc/api/v1/donator-status \
  -H 'content-type: application/json' \
  --data '{
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "chainId":137
  }'
```

Expected key fields:

- `donator.isDonator`
- `donator.discountPercent`
- `donator.validUntil`
- `donatorPolicy.minimumDonationUsd`
- `donatorPolicy.walletAddressByChain`

### Important: How to Donate Correctly

To register donor status onchain, donation must use contract method `donate` (or `donateFor`).

- Correct: call `donate(token, amount)` on donation registry contract for that network.
- Not enough: direct ERC20 transfer to the contract (this does not update donor state in registry logic).

Donation ABI methods:

- `donate(address token, uint256 amount)`
- `donateFor(address beneficiary, address token, uint256 amount)`
- `isDonorNow(address account)`

Network note:

- Registry contract can differ by chain. Use `GET /dapp-vurto/config` and read:
  - `donationPolicy.walletAddressByChain`
  - `donationPolicy.acceptedTokensByNetwork`

### Donor Tier Matrix (Current Policy)

Current configured tiers in backend policy:

| Tier | Minimum donation (USD) | Donor validity |
|---|---:|---:|
| 1 | 0.99 | 30 days |
| 2 | 1.95 | 60 days |
| 3 | 5.00 | 180 days |
| 4 | 10.00 | 365 days |

Important behavior:

- Base platform fee is `0.025%` (2.5 bps).
- Verified donor gets `100%` discount on platform fee (effective fee `0`).
- A qualifying donation sets/extends donor validity (`donator.validUntil`) according to its tier.
- Higher qualifying donation tiers can increase validity window.
- Confirm active state from API fields: `donator.isDonator`, `donator.validUntil`, `donator.activeTierMinUsd`, `donator.activeTierValidityDays`.

After onchain donation confirmation, refresh donor state:

```bash
curl -sS https://ana.vurto.cc/api/v1/donator-status \
  -H 'content-type: application/json' \
  --data '{
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "chainId":137,
    "force":true
  }'
```

## 5. Command Index

**Same-chain operations** (single transaction on one chain):

1. `supply_collateral` — deposit token as collateral
2. `supply_collateral_zap_in` — swap token A → supply token B as collateral
3. `borrow` — borrow token against collateral
4. `withdraw_supplied` — withdraw collateral
5. `withdraw_supplied_zap_out` — withdraw collateral → swap to another token
6. `switch_supplied` — swap supplied collateral to a different token
7. `repay_borrowed` — repay debt with the same token
8. `switch_borrowed` — swap debt to a different token (repay-with-swap)
9. `dual_swap` — switch both collateral AND debt in one transaction

**Crosschain operations** (non-atomic, multi-step across two chains via bridge):

10. `crosschain_supply` — withdraw from chain A → bridge → supply on chain B
11. `crosschain_repay` — borrow on chain A → bridge → repay debt on chain B
12. `position_migration` — alternate crosschain supply + crosschain repay cycles to move an entire position from chain A to chain B

---

## 6. Commands and Examples

### 6.1 Supply de collateral (`supply_collateral`)

Endpoint:

- `POST /build/supply`

Example:

```bash
curl -sS https://ana.vurto.cc/api/v1/build/supply \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "poolAddress":"0x794a61358d6845594f94dc1db02a252b5b4814ad",
    "token":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "amount":"1000000"
  }'
```

### 6.2 Supply com zap in (`supply_collateral_zap_in`)

Flow:

1. `POST /quote/collateral`
2. `POST /build/collateral/paraswap` (swap)
3. `POST /build/supply` (deposit target token)

Quote:

```bash
curl -sS https://ana.vurto.cc/api/v1/quote/collateral \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "userAddress":"0x1111111111111111111111111111111111111111",
    "fromToken":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "toToken":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
    "srcAmount":"1000000"
  }'
```

Build swap (use `priceRoute` from quote):

```bash
curl -sS https://ana.vurto.cc/api/v1/build/collateral/paraswap \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "userAddress":"0x1111111111111111111111111111111111111111",
    "fromToken":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "toToken":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
    "srcAmount":"1000000",
    "slippageBps":3,
    "priceRoute": {"...":"from quote"}
  }'
```

Then build supply with target token amount (`destAmount` or conservative min output):

```bash
curl -sS https://ana.vurto.cc/api/v1/build/supply \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "poolAddress":"0x794a61358d6845594f94dc1db02a252b5b4814ad",
    "token":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
    "amount":"995000"
  }'
```

### 6.3 Borrow (`borrow`)

Endpoint:

- `POST /build/borrow`

```bash
curl -sS https://ana.vurto.cc/api/v1/build/borrow \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "poolAddress":"0x794a61358d6845594f94dc1db02a252b5b4814ad",
    "token":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "amount":"1000000",
    "interestRateMode":2
  }'
```

### 6.4 Withdrawn de supplied (`withdraw_supplied`)

Endpoint:

- `POST /build/withdraw`

```bash
curl -sS https://ana.vurto.cc/api/v1/build/withdraw \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "poolAddress":"0x794a61358d6845594f94dc1db02a252b5b4814ad",
    "token":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "amount":"1000000",
    "recipient":"0x1111111111111111111111111111111111111111"
  }'
```

For full balance where supported:

- add `"useMaxAmount": true`

### 6.5 Withdrawn de supplied com zap out (`withdraw_supplied_zap_out`)

Flow:

1. `POST /build/withdraw`
2. `POST /quote/collateral`
3. `POST /build/collateral/paraswap`

Withdraw build:

```bash
curl -sS https://ana.vurto.cc/api/v1/build/withdraw \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "poolAddress":"0x794a61358d6845594f94dc1db02a252b5b4814ad",
    "token":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "amount":"1000000",
    "recipient":"0x1111111111111111111111111111111111111111"
  }'
```

Then quote/build collateral swap from withdrawn token to desired token.

### 6.6 Switch de supplied (`switch_supplied`)

Flow:

1. `POST /quote/collateral`
2. `POST /build/collateral/paraswap`

```bash
curl -sS https://ana.vurto.cc/api/v1/quote/collateral \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "userAddress":"0x1111111111111111111111111111111111111111",
    "fromToken":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "toToken":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
    "srcAmount":"1000000"
  }'
```

### 6.7 Repay de borrowed (`repay_borrowed`)

Endpoint:

- `POST /build/repay`

```bash
curl -sS https://ana.vurto.cc/api/v1/build/repay \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "poolAddress":"0x794a61358d6845594f94dc1db02a252b5b4814ad",
    "token":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "amount":"1000000",
    "interestRateMode":2
  }'
```

### 6.8 Switch de borrowed (`switch_borrowed`)

Flow:

1. `POST /quote/debt`
2. `POST /build/debt/paraswap`

```bash
curl -sS https://ana.vurto.cc/api/v1/quote/debt \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "userAddress":"0x1111111111111111111111111111111111111111",
    "fromToken":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "toToken":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
    "destAmount":"1000000"
  }'
```

### 6.9 Dual swap (`dual_swap`)

A dual swap swaps a **collateral pair** and a **debt pair** in **one atomic tx**. The
`/quote-and-build/dual` (and `/build/dual/aave-adapter`) response is a **single tx** —
`to`/`data`/`value`, **no `steps`**. Only the executor fallback may return `steps[]`.

> **Five rules — follow them and a dual swap works on EVERY chain/token; skip one and it
> reverts in a way that looks like an "adapter bug" but is not (the server resolves every
> address per-chain; the failures below are client behavior).**
>
> 1. **Approve both tokens the build names, once, max.** `requirements.collateralApproveToken`
>    (aToken — `approve`) and `requirements.debtDelegationToken` (variableDebtToken —
>    `approveDelegation`). The delegation token is the NEW debt and **FLIPS each direction** —
>    reusing the other direction's reverts with `InsufficientBorrowAllowance`.
> 2. **Use `recommendedGasLimit` (~4M).** A low cap (e.g. 1M) is out-of-gas — an EMPTY revert
>    with `gasUsed ≈ gasLimit`, not an adapter failure.
> 3. **Sign `to`/`data`/`value` VERBATIM.** Never reconstruct the call from `actions`/
>    `requirements` fields; passing an aToken where an underlying asset is expected reverts with
>    `InvalidAssetAddress`. The build calldata is simulation-verified.
> 4. **Sign with `@vurto/sign-tx`** (the version `prepare_signing` returns). It simulates and
>    decodes the revert reason BEFORE broadcasting, so you never spend gas on a guaranteed
>    revert and you get an actionable message instead of "no decoded error".
> 5. These rules are **chain- and token-agnostic.** WBTC/cbBTC/cbETH at tiny amounts just need
>    more `slippageBps` (volatile, low-decimal); everything else is identical across chains.

**Required approvals (grant once, to the adapter).** The adapter pulls your collateral aToken
and opens the new debt on your behalf, so before the swap two approvals must already exist —
the build does **not** include them:

| Approval | On token | Grants |
|----------|----------|--------|
| `approve(adapter, max)` | aToken of `collateral.fromToken` | adapter can withdraw + swap your collateral |
| `approveDelegation(adapter, max)` | variableDebtToken of `debt.toToken` | adapter can borrow the new debt for you |

`adapter` = `adapterAddress` in the build response. aToken / variableDebtToken addresses come
from `get_position` (`supplies[]` / `borrows[]` / `marketAssets[]`) — never hardcode. If
either approval is missing, the swap **reverts at gas estimation** with an opaque
`execution reverted (unknown custom error)` (nothing broadcast). So **preflight**: read each
allowance (1 RPC call each) and grant only the ones that read `0`. Max-approve once and every
future dual swap of that config is a single signature; the **opposite direction** uses
different tokens and needs its own two approvals.

```js
// preflight: approve only what is 0
// use the Vurto RPC for the chain (see "RPC endpoints" in the signing section),
// e.g. const provider = new JsonRpcProvider("https://rpc.vurto.cc/polygon");
const aTok  = new Contract(aCollateral, ["function allowance(address,address) view returns (uint256)","function approve(address,uint256) returns (bool)"], wallet);
if ((await aTok.allowance(wallet.address, adapter)) === 0n) await (await aTok.approve(adapter, MaxUint256)).wait(1);
const vDebt = new Contract(vNewDebt, ["function borrowAllowance(address,address) view returns (uint256)","function approveDelegation(address,uint256)"], wallet);
if ((await vDebt.borrowAllowance(wallet.address, adapter)) === 0n) await (await vDebt.approveDelegation(adapter, MaxUint256)).wait(1);
```

With approvals in place the flow is: build → sign once (see the signing section) → done.
Collateral leg uses `srcAmount`; debt leg uses `destAmount`. A reverting swap with
`unknown custom error` almost always means a missing approval — re-run the preflight; never
raise slippage to force it.

**Full position.** You may pass the entire aToken balance as `collateral.srcAmount`: the
backend automatically caps it slightly to leave the Aave flash-loan premium. (The adapter
flash-borrows the collateral and repays `flashAmount + premium` by withdrawing your aToken, so
a literal 100% collateral leg has nothing left for the premium and reverts with
`TokenOperationFailed(aToken)`.) The debt leg may be repaid 100% — the new debt is sized to
include the premium.

**`requirements` block (in the build response).** Tells you the premium-inclusive minimums the
two approvals to the adapter must cover, so you never under-approve:

```json
"requirements": {
  "flashPremiumBufferBps": 10,
  "collateralApproveToken": "0x078f…",        // call aToken.approve(adapter, …) on THIS token
  "collateralRequiredAllowance": "3902",      // aToken.approve(adapter, >= this)
  "debtDelegationToken": "0xfb00…",           // call approveDelegation(adapter, …) on THIS token
  "debtRequiredBorrowAllowance": "525432",    // approveDelegation(adapter, >= this)
  "recommendation": "max-approve the collateral aToken and max-delegate the new debt to the adapter, once"
}
```

**Delegate the token the build names, not the old one.** `debtDelegationToken` is the
variableDebtToken of the NEW debt (`debt.toToken`) — the adapter borrows that to repay the old
debt. **Flipping direction swaps which token that is**: a LONG→SHORT flip delegates one, the
SHORT→LONG flip delegates the other. Reusing the previous direction's delegation reverts with
`InsufficientBorrowAllowance(current=0, required=…)`. Always delegate `requirements.debtDelegationToken`.

Approving only the principal reverts with `TokenOperationFailed` (collateral) or
`InsufficientBorrowAllowance` (debt). **Max-approve once** is recommended — exact amounts also
drift as the aToken accrues interest between approve and execution. When a sign reverts,
`@vurto/sign-tx` (>= 0.1.3) decodes the selector into an actionable message
(e.g. `InsufficientBorrowAllowance — delegate at least <amount>`) instead of `unknown custom error`.

**Odd-length `data` = corrupted in transit, not a server bug.** The build asserts whole-byte,
word-aligned calldata server-side before returning (a regression would surface as a `502
odd_calldata`, never a malformed payload). So if your client receives `data` with an odd number
of hex characters and ethers throws `invalid BytesLike value`, a nibble was dropped in transit
(`0xb8bd1c6b` → `0x0b8bd1c6b`, shifting the selector). **Re-fetch the build and copy `data`
verbatim — never pad or patch the hex.**

**Gas — a dual swap uses ~2.8-3.0M, but set the limit to ~4M.** It runs a flash loan + two DEX
swaps + supply + repay in one tx; the flash-loan 63/64 gas rule means the LIMIT must sit well
above the used gas (observed: a 3M limit reverted at 2.97M used; a 4M limit succeeded at 2.79M).
The build response includes `recommendedGasLimit` (4,000,000) — set the tx `gasLimit` to at least
that, or let the signer estimate and add headroom. **Never hard-cap it low** (e.g. 1M):
the flash-loan callback hits the EIP-150 63/64 gas wall and reverts with an **empty reason** (no
decoded error, `gasUsed` near your limit). An undecoded revert with `gasUsed ≈ gasLimit` is
out-of-gas — raise the limit, it is not an adapter bug. `@vurto/sign-tx` auto-estimates; raw
clients must not undercut.

**Submit the build `data` verbatim — do NOT reconstruct the adapter call.** The build emits the
complete, simulation-verified calldata. Do not rebuild it from the `actions`/`requirements`
fields. In particular, `collateralApproveToken`/`debtDelegationToken` are the **aToken /
variableDebtToken to APPROVE** — they are NOT swap or asset parameters. Passing one of them (an
aToken) where an underlying reserve asset is expected reverts with
`InvalidAssetAddress(aToken)` — that is a client-side reconstruction bug, not the adapter (the
build’s own calldata simulates fine). Sign the build’s `to`/`data`/`value` as-is.

Preferred flow:

1. `POST /quote/dual`
2. `POST /build/dual/aave-adapter`
3. Fallback: `POST /build/dual/executor` if adapter route is unavailable on chain.

Dual quote:

```bash
curl -sS https://ana.vurto.cc/api/v1/quote/dual \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "userAddress":"0x1111111111111111111111111111111111111111",
    "collateral":{
      "fromToken":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
      "toToken":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
      "srcAmount":"1000000"
    },
    "debt":{
      "fromToken":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
      "toToken":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
      "destAmount":"1000000"
    }
  }'
```

Dual build payload accepts backward-compatible quote-driven shape with `priceRoute` for both legs.

### 6.9.1 Compound quote+build endpoints (recommended)

For agents that do not need to inspect the quote before building, three compound endpoints run quote and build server-side in one round-trip. This avoids the ParaSwap `priceRoute` TTL race between two separate calls.

Endpoints:

- `POST /quote-and-build/collateral` — replaces `/quote/collateral` + `/build/collateral/paraswap`
- `POST /quote-and-build/debt` — replaces `/quote/debt` + `/build/debt/paraswap`
- `POST /quote-and-build/dual` — replaces `/quote/dual` + `/build/dual/aave-adapter` (with executor fallback)

Request bodies match the existing `/quote/*` request shapes.

Response shape: the existing `build_*` response (`to`, `data`, `value`, `transactionId`, `approval`, `steps`, `feeBps`, `feeAmount`, `_meta`) **plus** a new top-level `quote` object:

```json
{
  "to": "0x...",
  "data": "0x...",
  "value": "0",
  "transactionId": "f07544a0-12f9-4dc8-9d6f-b92280c38828",
  "feeBps": 2.5,
  "feeAmount": "2500",
  "quote": {
    "srcAmount": "1000000",
    "destAmount": "999300",
    "gasEstimate": "210000",
    "route": { "...": "opaque" },
    "priceImpactBps": 1.2,
    "recommendedSlippageBps": 30
  }
}
```

For `/quote-and-build/dual`, `quote` carries per-leg sub-objects: `quote.collateral` and `quote.debt`, each with the same shape as the single-leg `quote` above.

Example:

```bash
curl -sS https://ana.vurto.cc/api/v1/quote-and-build/collateral \
  -H 'content-type: application/json' \
  --data '{
    "chainId":137,
    "walletAddress":"0x1111111111111111111111111111111111111111",
    "userAddress":"0x1111111111111111111111111111111111111111",
    "fromToken":{"address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"symbol":"USDC"},
    "toToken":{"address":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","decimals":6,"symbol":"USDT"},
    "srcAmount":"1000000",
    "slippageBps":30
  }'
```

### 6.9.2 `user_notice` for local-signer flows

When an agent delegates signing to the local signer (via the MCP `prepare_signing` tool), the response includes a `cli.user_notice` string. The agent **must relay this notice verbatim to the user before printing the local signer URL**. It explains that the URL is local, why the browser may show "unknown application" warnings, and that the signer enforces wallet/chain match before submitting. Do not paraphrase or omit it.

### 6.10 Crosschain Supply (`crosschain_supply`) — Non-Atomic

> **Do not confuse with same-chain operations.** `build_supply`, `build_borrow`, `build_withdraw`, and `build_repay` operate on a single chain. Crosschain Supply and Crosschain Repay are **multi-chain** operations that move funds between two different chains via a bridge.

**Crosschain Supply** withdraws collateral from the source chain, bridges it, and supplies it on the destination chain. Use `intent: "supply"` with `sourceFunding.mode: "withdraw"` (or `"withdraw_and_zap_out"` if the token is not bridge-compatible).

This is the fundamental building block for moving collateral from one chain to another.

Flow overview:

1. `POST /quote/longshot` — get crosschain route quote (auto-selects bridge provider: Across or USDT0)
2. `POST /build/longshot` — build multi-step execution with quoteId
3. Execute **source step**: sign and send the borrow/withdraw tx on source chain
4. Report: `POST /longshot/executions/:id/event` with `event: source_submitted` + `txHash`
5. Wait for source confirmation and report: `event: source_confirmed`
6. Execute **bridge step**: sign approve + deposit txs on source chain (if zap-out needed, also sign swap txs first; if non-donor, execute fee transfer before deposit)
7. Poll: `GET /longshot/executions/:id/bridge-check` until `bridged: true`
8. Execute **destination step**: sign and send supply tx on destination chain
9. Report: `event: destination_submitted` + `txHash`, then `event: destination_confirmed`

**Important rules for agents:**

- This is a **non-atomic** operation — each step is a separate onchain transaction.
- The execution may need **zap-out** (source token → bridge stable) and/or **zap-in** (bridge stable → destination token) if the tokens are not the same stablecoin family.
- The `projectedSourceHealthFactor` must be >= ~1.08 or the quote will be rejected.
- Quote expires in ~5 minutes from creation.
- If any step fails, use `POST /longshot/executions/:id/recover` with the appropriate action (`retry_source`, `retry_bridge`, `retry_destination`, or `mark_abandoned`).
- Bridge fee: non-donors pay 2.5 bps bridge fee transfer before the deposit. Donors pay 0.

Quote:

```bash
curl -sS https://ana.vurto.cc/api/v1/quote/longshot \
  -H 'content-type: application/json' \
  --data '{
    "walletAddress": "0x1111111111111111111111111111111111111111",
    "intent": "supply",
    "sourceChainId": 42161,
    "destinationChainId": 137,
    "sourceFunding": {
      "mode": "borrow",
      "token": {"address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", "decimals": 6, "symbol": "USDC"},
      "amount": "1000000",
      "poolAddress": "0x794a61358d6845594f94dc1db02a252b5b4814ad",
      "projectedSourceHealthFactor": 1.5,
      "interestRateMode": 2
    },
    "destinationAction": {
      "token": {"address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "decimals": 6, "symbol": "USDC"},
      "amount": "990000",
      "poolAddress": "0x794a61358d6845594f94dc1db02a252b5b4814ad"
    },
    "slippageBps": 30
  }'
```

Build from quote:

```bash
curl -sS https://ana.vurto.cc/api/v1/build/longshot \
  -H 'content-type: application/json' \
  --data '{
    "quoteId": "lq_...",
    "walletAddress": "0x1111111111111111111111111111111111111111"
  }'
```

Response contains `executionId`, `steps` array (source, bridge, destination), `statusUrl`, and `recoverUrl`.

Report source tx submitted:

```bash
curl -sS https://ana.vurto.cc/api/v1/longshot/executions/lex_.../event \
  -H 'content-type: application/json' \
  --data '{
    "walletAddress": "0x1111111111111111111111111111111111111111",
    "event": "source_submitted",
    "txHash": "0xabc..."
  }'
```

Check bridge arrival:

```bash
curl -sS "https://ana.vurto.cc/api/v1/longshot/executions/lex_.../bridge-check?walletAddress=0x1111111111111111111111111111111111111111"
```

Poll until `bridged: true`, then execute destination tx and report `destination_submitted` → `destination_confirmed`.

Get execution status:

```bash
curl -sS "https://ana.vurto.cc/api/v1/longshot/executions/lex_.../status?walletAddress=0x1111111111111111111111111111111111111111"
```

Recover from failure:

```bash
curl -sS https://ana.vurto.cc/api/v1/longshot/executions/lex_.../recover \
  -H 'content-type: application/json' \
  --data '{
    "walletAddress": "0x1111111111111111111111111111111111111111",
    "action": "retry_source"
  }'
```

### 6.11 Crosschain Repay (`crosschain_repay`) — Non-Atomic

**Crosschain Repay** borrows on the source chain, bridges the funds, and repays debt on the destination chain. Use `intent: "repay"` with `sourceFunding.mode: "borrow"`.

This is the complement of Crosschain Supply — it allows paying down debt on one chain using borrowing capacity from another.

Same execution flow as Crosschain Supply (quote → build → source step → bridge → destination step).

Quote example:

```bash
curl -sS https://ana.vurto.cc/api/v1/quote/longshot \
  -H 'content-type: application/json' \
  --data '{
    "walletAddress": "0x1111111111111111111111111111111111111111",
    "intent": "repay",
    "sourceChainId": 42161,
    "destinationChainId": 137,
    "sourceFunding": {
      "mode": "withdraw",
      "token": {"address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", "decimals": 6, "symbol": "USDC"},
      "amount": "2000000",
      "poolAddress": "0x794a61358d6845594f94dc1db02a252b5b4814ad",
      "projectedSourceHealthFactor": 2.0,
      "interestRateMode": 2
    },
    "destinationAction": {
      "token": {"address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "decimals": 6, "symbol": "USDC"},
      "amount": "1950000",
      "poolAddress": "0x794a61358d6845594f94dc1db02a252b5b4814ad",
      "interestRateMode": 2
    },
    "slippageBps": 30
  }'
```

### 6.12 Position Migration — Moving an Entire Position Between Chains

When a user has both collateral and debt on chain A and wants to move everything to chain B, use alternating **Crosschain Supply** and **Crosschain Repay** cycles:

1. **Crosschain Supply** — withdraw a safe portion of collateral from A → bridge → supply on B.
2. **Crosschain Repay** — borrow on B (now possible thanks to step 1 collateral) → bridge → repay debt on A. This restores chain A's health factor.
3. **Repeat** steps 1–2, each cycle moving more collateral and reducing more debt.
4. **Final step is always a Crosschain Supply** — once all debt on A is repaid, withdraw the remaining collateral from A and supply it on B.

**Key rules:**
- Call `GET /position` on both chains between cycles to recalculate safe withdrawal/borrow amounts and health factors.
- The `projectedSourceHealthFactor` must stay >= ~1.08 at every step.
- The number of cycles depends on the position's loan-to-value ratio — higher leverage requires more, smaller cycles.
- Each Crosschain Supply or Crosschain Repay is a full multi-step execution (source tx → bridge → destination tx) that must complete before starting the next.

### Crosschain Execution States

| State | Meaning |
|---|---|
| `awaiting_source_signature` | Waiting for agent to sign source tx |
| `source_submitted` | Source tx sent, awaiting confirmation |
| `source_confirmed` | Source tx confirmed onchain |
| `source_failed` | Source tx reverted — recoverable via `retry_source` |
| `bridge_pending` | Bridge deposit in progress |
| `bridge_confirmed` | Funds arrived on destination chain |
| `bridge_failed` | Bridge issue — recoverable via `retry_bridge` |
| `awaiting_destination_signature` | Waiting for agent to sign destination tx |
| `destination_submitted` | Destination tx sent |
| `destination_confirmed` | Destination tx confirmed — operation complete |
| `destination_failed` | Destination tx failed — recoverable via `retry_destination` |
| `completed` | Full crosschain operation finished |
| `abandoned` | User abandoned the execution |

### Crosschain Error Classes

- `422 longshot_source_hf_guardrail` — projected source health factor too low
- `422 longshot_destination_guardrail` — destination reserve frozen or disabled
- `422 longshot_no_route` — no bridge route available for chain pair
- `422 longshot_provider_override_unavailable` — forced provider not available
- `410 longshot_quote_expired` — quote TTL exceeded (re-quote needed)
- `403 longshot_wallet_mismatch` — build/event wallet differs from quote wallet
- `503 longshot_disabled` — crosschain feature disabled by runtime config

## 7. Transaction Tracking Hooks (Optional but Recommended)

If build response includes `transactionId`, track state. `transactionId` is a UUID v4 string (e.g. `f07544a0-12f9-4dc8-9d6f-b92280c38828`) — pass it verbatim in the URL. Legacy integer IDs are still accepted as input for backward compatibility, but new build responses always return strings.

- Sent hash:

```bash
curl -sS https://ana.vurto.cc/api/v1/transactions/f07544a0-12f9-4dc8-9d6f-b92280c38828/send-hash \
  -H 'content-type: application/json' \
  --data '{"txHash":"0xabc..."}'
```

- Confirmed:

```bash
curl -sS https://ana.vurto.cc/api/v1/transactions/f07544a0-12f9-4dc8-9d6f-b92280c38828/confirm \
  -H 'content-type: application/json' \
  --data '{"gasUsed":"210000","actualPaid":"1000000"}'
```

- Rejected:

```bash
curl -sS https://ana.vurto.cc/api/v1/transactions/f07544a0-12f9-4dc8-9d6f-b92280c38828/reject \
  -H 'content-type: application/json' \
  --data '{"reason":"user_rejected"}'
```

- Failed:

```bash
curl -sS https://ana.vurto.cc/api/v1/transactions/f07544a0-12f9-4dc8-9d6f-b92280c38828/fail \
  -H 'content-type: application/json' \
  --data '{"reason":"execution_failed"}'
```

History:

- `GET /transactions/user/:walletAddress?limit=50`

## 7.1 MCP Tool `prepare_signing` (Hardware Wallet Flow)

For agents speaking MCP to the Vurto AnA server (`https://ana.vurto.cc/mcp`): when the user holds a hardware wallet (Ledger/Trezor) or asks to sign via a browser wallet, the agent cannot inject a private key. Use the MCP tool `prepare_signing` to obtain a CLI invocation that drives a local browser signer.

This tool does NOT call any backend endpoint. It synthesizes the CLI invocation inline from its arguments plus the worker's pinned CLI version (`VURTO_SIGN_CLI_VERSION` env var).

Tool name: `prepare_signing`

Request (MCP `tools/call` arguments):

```json
{
  "transactionId": 12345,
  "txData": {
    "to": "0x794a61358D6845594F94dc1DB02A252b5b4814aD",
    "data": "0x617ba037...",
    "chainId": 42161,
    "value": "0x0"
  },
  "walletAddress": "0x12426b77FC96a5250f9A2d27dA3B26e826af5Dc0",
  "humanSummary": "Supply 100 USDC on Arbitrum to AAVE V3"
}
```

Required fields: `transactionId` (number, from a prior `build_*`), `txData.to`, `txData.data`, `txData.chainId`, `walletAddress`. Optional: `txData.value` (defaults to `"0x0"`), `humanSummary` (recommended — shown to the user on the signer page).

Response (single content item, `type: text`, with a JSON string body):

```json
{
  "stdin_payload": "<base64-encoded-json>",
  "methods": {
    "browser": { "recommended": true, "command": "npx @vurto/sign-tx@<version>", "args": ["--timeout", "300"] },
    "key": { "command": "npx @vurto/sign-tx@<version>", "args": ["--mode", "key"] },
    "fast": {
      "url": "https://ana.vurto.cc/cli/sign-tx-<version>.cjs",
      "sha256": "<hex>",
      "shell": "f=\"$(mktemp)\" && curl -fsSL <url> -o \"$f\" && echo \"<sha256>  $f\" | sha256sum -c - >/dev/null && node \"$f\" --timeout 300; rc=$?; rm -f \"$f\"; exit $rc"
    }
  },
  "cli": {
    "package": "@vurto/sign-tx",
    "version": "<version>",
    "command": "npx @vurto/sign-tx@<version>",
    "args": ["--timeout", "300"],
    "stdin_payload": "<base64-encoded-json>",
    "parse_stdout_as_json": true,
    "expected_keys": ["txHash", "from"],
    "exit_codes": { "0": "signed", "1": "rejected", "2": "timeout", "3": "error" }
  }
}
```

Pick one `methods` entry (the `cli` block is a flat back-compat mirror of `browser`):

- `browser` (default): `npx @vurto/sign-tx@<version>` over npm — portable (sh/Windows), version-pinned, cosign-verifiable.
- `key`: same npm CLI with `--mode key` for headless signing (the key is typed into the user's terminal, never the chat).
- `fast` (when present): the **same signer fetched from this MCP's own origin** (`/cli/sign-tx-<version>.cjs`) instead of npm — no dependency install, faster cold start. Run `fast.shell` via `sh -c`; it downloads to a temp file, verifies `fast.sha256`, runs it, and cleans up (POSIX sh). Trust is HTTPS to the same origin serving this API. Prefer it for latency; use `browser` for npm's independent integrity check or on Windows.

#### RPC endpoints — always use the Vurto RPC for the network

For both the `key`-mode signer and any preflight on-chain reads (allowances, balances), **always
use the Vurto RPC that matches the chain** instead of a random public node — it is the
load-balanced, failover-backed endpoint this backend itself uses. Set `VURTO_RPC_<chainId>` (the
signer reads it; you can also pass `--rpc`) and point your own ethers provider at the same URL:

| chainId | Network | Endpoint |
|--------:|---------|----------|
| 1 | Ethereum | `https://rpc.vurto.cc/eth` |
| 10 | Optimism | `https://rpc.vurto.cc/optimism` |
| 56 | BNB Chain | `https://rpc.vurto.cc/bnb` |
| 100 | Gnosis | `https://rpc.vurto.cc/gnosis` |
| 137 | Polygon | `https://rpc.vurto.cc/polygon` |
| 146 | Sonic | `https://rpc.vurto.cc/sonic` |
| 8453 | Base | `https://rpc.vurto.cc/base` |
| 42161 | Arbitrum | `https://rpc.vurto.cc/arbitrum` |
| 43114 | Avalanche | `https://rpc.vurto.cc/avalanche` |

```bash
# headless signer + preflight reads on, e.g., Arbitrum (42161)
export VURTO_RPC_42161="https://rpc.vurto.cc/arbitrum"
```

`stdin_payload` is the base64 encoding of `{ txData, summary: { humanDescription }, walletAddress, transactionId }`. Whichever method you pick, pipe this string verbatim into the process's stdin — the CLI base64-decodes it internally. Do NOT decode it before piping.

CLI stdout on success (one line of JSON):

```json
{ "txHash": "0x...", "from": "0x...", "chainId": 42161, "signedAt": "2026-05-27T15:42:00Z" }
```

After receiving the hash, the agent posts it to the existing tracking endpoint:

```bash
curl -sS https://ana.vurto.cc/api/v1/transactions/12345/send-hash \
  -H 'content-type: application/json' \
  --data '{"txHash":"0x..."}'
```

Version pinning is mandatory: always invoke the exact version returned by the tool, never `@latest`. See `SECURITY.md` for supply-chain verification steps.

Validation errors returned by the tool (as `isError: true`): missing or non-numeric `transactionId`, malformed `walletAddress`, malformed `txData.to` / `txData.data`, or non-integer `txData.chainId`.

## 8. Minimum Pre-Trade Checklist for Agent

Before any build:

1. `POST /position` to get user exposure and available assets.
2. `POST /donator-status` to resolve fee profile.
3. `GET /dapp-vurto/config` to read donation policy, supported dual chains, and runtime limits.
4. For swap flows, always run quote right before build.
5. Reject execution if route impact is too high or payload is missing required addresses.

## 9. Common Error Classes

- `400 invalid_payload` or `invalid_amount`: malformed input.
- `400 unsupported_chain`: dual mode not configured on selected chain.
- `403 cors_origin_not_allowed`: origin not allowed by backend policy.
- `403 target_not_allowed` / `aggregator_not_allowed` / `spender_not_allowed`: dual allowlist enforcement.
- `422 quote_impact_too_high`: risk guardrail triggered.
- `429 rate_limited`: request throttle.

## 10. Response Fields Agents Should Persist

For every successful build, store at least:

- `to`
- `data`
- `value`
- `transactionId` (UUID v4 string; legacy integer form still accepted as input to `/transactions/:id/*`)
- `feeBps`
- `feeAmount`
- `_meta` (contains backend mode and donation policy context)
