
# Hopivia API

Hopivia gives AI agents, trading bots and scripts the same thing the [web app](https://hopivia.com/) gives people: the best price across every major exchange of a network, a Tron ↔ EVM USDT bridge and token safety checks. You get plain JSON and **unsigned transactions**. Three ways to use it:

1. **Quick start** — quote, build, sign from the agent's own wallet.
2. **[Delegated access](#delegated-access)** — a user gives your agent a limit on their wallet instead of a key: which tokens, how much per day or in total, until when. A smart contract enforces it.
3. **[MCP server](#mcp-server)** — one command, and Hopivia becomes a tool in Claude, Cursor and other assistants.

> **Your keys stay with you.** The API never asks for a private key and never holds funds. Every write endpoint returns transactions that your agent signs with its own wallet and sends itself. Swaps go only to the router contracts Hopivia allows, and the tokens go to the address you choose.

| | |
|---|---|
| Base URL | `https://hopivia.com/api/v1` |
| Format | JSON, UTF-8 |
| Auth | optional `X-API-Key` header — [get a free key](https://hopivia.com/agents#keys) |
| Limit instead of a key | [https://hopivia.com/agents](https://hopivia.com/agents) — the user sets it there, see [Delegated access](#delegated-access) |
| This guide as Markdown | [`https://hopivia.com/docs.md`](https://hopivia.com/docs.md) — give it to your model |
| OpenAPI 3.1 | [`https://hopivia.com/api/openapi.json`](https://hopivia.com/api/openapi.json) · [interactive reference](https://hopivia.com/api/docs) |
| Discovery | [`/llms.txt`](https://hopivia.com/llms.txt) · [`/.well-known/agent-card.json`](https://hopivia.com/.well-known/agent-card.json) |

**Same-chain swaps:** BNB Chain, Ethereum, Arbitrum, Base, Polygon, Optimism, Avalanche. **Cross-chain:** any pair of the networks above, plus opBNB and Tron (USDT only on Tron).

## Quick start

### 1. Ask for a price

No key is needed to try it.

```bash
curl "https://hopivia.com/api/v1/quote?chain=bsc&tokenIn=BNB&tokenOut=USDT&amount=0.5"
```

The answer names the exchange with the best output, the route, the minimum you will receive at the chosen slippage and the price impact:

```json
{
  "best": {
    "venue": "PancakeSwap V3",
    "label": "BNB → 0.01% → USDT",
    "amountOut": { "wei": "355113773306503027910", "formatted": "355.11377330650302791" },
    "minOut":    { "wei": "353338204439970512770", "formatted": "353.33820443997051277" },
    "feeBps": 0
  },
  "alternatives": [
    { "venue": "Uniswap V3", "amountOut": { "formatted": "355.025274019855417697" } },
    { "venue": "PancakeSwap", "amountOut": { "formatted": "353.425882806009630914" } }
  ],
  "venuesCompared": 9,
  "priceImpactPct": 0.01
}
```

### 2. Build the transactions

Pass the wallet that will sign as `from`. The server quotes again, checks the balance and the token, and returns the steps in order.

```bash
curl -X POST "https://hopivia.com/api/v1/swap/tx" \
  -H "content-type: application/json" \
  -H "x-api-key: $HOPIVIA_KEY" \
  -d '{"chain":"bsc","tokenIn":"USDT","tokenOut":"BNB","amount":"25","from":"0xYourAgentWallet"}'
```

```json
{
  "steps": [
    { "kind": "approve", "description": "Approve USDT to PancakeSwap V3 router", "chainId": 56,
      "to": "0x55d398326f99059fF775485246999027B3197955", "data": "0x095ea7b3…", "value": "0" },
    { "kind": "swap", "description": "Swap USDT → BNB via PancakeSwap V3", "chainId": 56,
      "to": "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4", "data": "0xac9650d8…", "value": "0" }
  ],
  "minOut": { "formatted": "0.035018498007912409" },
  "simulation": { "ok": false, "skipped": "approval step must be mined first" },
  "warnings": []
}
```

### 3. Sign and send, in order

```js
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { bsc } from 'viem/chains';

const BASE = 'https://hopivia.com/api/v1';
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY); // stays on your machine
const wallet = createWalletClient({ account, chain: bsc, transport: http() });
const chain = createPublicClient({ chain: bsc, transport: http() });

async function buildSwap(body) {
  const res = await fetch(`${BASE}/swap/tx`, {
    method: 'POST',
    headers: { 'content-type': 'application/json', 'x-api-key': process.env.HOPIVIA_KEY },
    body: JSON.stringify({ ...body, from: account.address }),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error}: ${json.message}`); // see "Errors and refusals"
  return json;
}

async function send(step) {
  const hash = await wallet.sendTransaction({ to: step.to, data: step.data, value: BigInt(step.value) });
  const receipt = await chain.waitForTransactionReceipt({ hash });
  if (receipt.status !== 'success') throw new Error(`${step.kind} reverted: ${hash}`);
  return hash;
}

const order = { chain: 'bsc', tokenIn: 'USDT', tokenOut: 'BNB', amount: '25' };
let plan = await buildSwap(order);

// Approvals first, then ask again: the new plan has a fresh price and a real dry run
const approvals = plan.steps.filter((s) => s.kind !== 'swap');
if (approvals.length > 0) {
  for (const step of approvals) await send(step);
  plan = await buildSwap(order);
}
if (!plan.simulation.ok) throw new Error(`not simulated: ${plan.simulation.skipped ?? plan.simulation.error}`);
const txHash = await send(plan.steps.find((s) => s.kind === 'swap'));
```

> **Why ask twice?** A swap built before its approval is mined cannot be simulated. After the approval, `/swap/tx` returns a single swap step with a fresh price and a successful dry run.

## Delegated access

A user can give your agent a **limit instead of a key**. On the [agents page](https://hopivia.com/agents#delegation) the user allows your agent's address on the Hopivia delegation contract: which tokens (one or several), how much, until when. Your agent then calls the contract's `swap()` from its own wallet; the contract pulls the input token from the user's wallet, swaps it on one of the allowed V2 exchanges and sends the output **only to the user's address**. The user can remove a token, an agent or everything at any time. All of this is enforced on-chain: the API is not involved and cannot override it, and the contract has no owner and no admin functions. On first-version networks no Hopivia fee is charged on delegated swaps; [version 2](#delegation-v2) executes through the Hopivia aggregator with its 0.15 % fee.

| Network | Chain ID | Contract | Exchanges |
|---|---|---|---|
| Ethereum | 1 | [`0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0`](https://etherscan.io/address/0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0) | 3 |
| Optimism | 10 | [`0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0`](https://optimistic.etherscan.io/address/0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0) | 2 |
| BNB Chain | 56 | [`0xeB2028f91A53d65F199fB90aAb4701C72bdC0E0f`](https://bscscan.com/address/0xeB2028f91A53d65F199fB90aAb4701C72bdC0E0f) | 7 |
| Polygon | 137 | [`0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0`](https://polygonscan.com/address/0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0) | 4 |
| Base | 8453 | [`0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0`](https://basescan.org/address/0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0) | 4 |
| Arbitrum | 42161 | [`0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0`](https://arbiscan.io/address/0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0) | 3 |
| Avalanche | 43114 | [`0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0`](https://snowtrace.io/address/0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0) | 2 |

### Delegation v2

Networks where `GET /chains` returns `venues.delegationV2: true` use the second version of the contract; elsewhere everything below about the first version applies. The request flow is the same (`GET /delegation`, then `POST /swap/tx` with `onBehalfOf`); what the owner sets and what the contract checks differ:

- **One limit in dollars per agent across all tokens**, either for any rolling 24 hours (25 hourly buckets, so a spend frees up on the hour it entered the window) or as a one-time budget that never resets. Optional: a per-trade cap in dollars and an owner-wide cap across all agents.
- **Two token lists.** Tokens the agent may *sell* count towards the dollar limit; tokens it may only *buy* cannot be sold from the owner's stock. The owner can instead allow selling or buying *any token that has a price source*. The network coin is received through its wrapped token, which the agents page adds to the buy list by default.
- **Price check on every trade.** An on-chain oracle (Chainlink feeds, a deep-pool TWAP where no feed exists) values the input in dollars and sets a price floor with the owner's slippage (1 % by default). A token without a price source cannot be sold.
- **Execution through the Hopivia aggregator**, V2 and V3 routes with at most one intermediate hop (the wrapped network coin, a hub stablecoin or a listed token). The 0.15 % fee is already in the quoted `amountOut` and `amountOutMin`.

`GET /delegation` returns `version: 2`, a `grant` object (`active`, `capUsd`, `maxTradeUsd`, `spentUsd`, `availableUsd` in dollars, `expiry`, `resetsAt`, `flags`: `oneTime` / `sellAny` / `buyAny` / `transfers`, `slippageBps`), `tokens[]` with `mode` (`sell`, `buy`, `noPriceCheck`), an optional per-token cap and the owner's `allowance`, plus `recipients`, `hubs` and the routers. `POST /swap/tx` returns `feeBps: 15`, `viaAggregator: true` and `delegated` with `version: 2`, `tradeUsd` (this trade in dollars), `availableUsd`, `expiry` and `resetsAt`.

Additional refusals on v2: `delegation_no_price` (no price source for `tokenIn`), `delegation_per_trade_cap`, `delegation_owner_cap_exceeded`; `delegation_cap_exceeded` carries `details.tradeUsd`, `details.availableUsd` and either `resetsAt` (24 h window) or `oneTime`. The contract exposes `preview(owner, agent, token, amount) → (usd, reason)`: the dollar value of a trade and the exact refusal reason (empty when it would pass) without sending a transaction.

The rest of this section describes the first version.

**Two kinds of limit.** *Per day*: the cap renews every UTC day until the expiry; the user's ERC-20 allowance to the contract is "daily cap × days". *One-time (total)*: the allowance is set to exactly the amount, so once the agent has sold it the limit is used up even before the expiry — `GET /delegation` then shows `allowance` below `availableToday`.

**What to ask the user for.** Your agent's address (the agents page can generate a fresh wallet for a bot that has none yet); the tokens you will sell *and* the tokens you will buy (both sides must be allowed; the network coin is always allowed as output); the cap per token and its kind; an expiry date. The agent pays gas for its own transactions, so its wallet needs a little of the network coin.

**Interface** (Solidity):

```text
function swap(address owner, address router, uint256 amountIn, uint256 amountOutMin,
              address[] path, bool toNative, uint256 deadline) returns (uint256 amountOut)
function limitOf(address owner, address agent, address token)
         returns (uint256 dailyCap, uint256 expiry, uint256 spentToday, uint256 availableToday, uint256 resetsAt)
function tokensOf(address owner, address agent) returns (address[])
function routers() returns (address[])        // the only exchanges the contract will call
```

**Trading flow through the API** (the API never signs; it checks the limits and builds the one contract call):

1. `GET /delegation?chain=bsc&owner=<owner>&agent=<your wallet>` — which tokens the owner allowed you, the daily cap, what is left today, the expiry and the owner's allowance to the contract. Also the contract address and the V2 routers it may call.
2. `POST /swap/tx` with `from` = your wallet and `onBehalfOf` = the owner. The API quotes only on the contract's routers, verifies the limits and returns a single `swap` step on the delegation contract. `tokenIn` must be an ERC-20 the owner allowed (the contract cannot pull the network coin, so the owner allows WBNB/WETH to sell BNB/ETH); `tokenOut` must be an allowed token or `native`. No Hopivia fee on delegated swaps.
3. Sign and send that step from your wallet. You pay the gas; the output goes to the owner. The response carries `delegated.availableToday` — what remains of the cap after this swap — and `delegated.resetsAt`.

```js
const d = await get('/delegation', { chain: 'bsc', owner, agent: agent.address });
// d.tokens → [{ token: { symbol: 'USDT', … }, availableToday: { formatted: '80' }, expiry, allowance, … }]
const plan = await post('/swap/tx', { chain: 'bsc', tokenIn: 'USDT', tokenOut: 'native', amount: '25', from: agent.address, onBehalfOf: owner });
await agent.sendTransaction(plan.steps[0]);   // one call: swap(owner, router, amountIn, minOut, path, toNative, deadline)
```

**Refusals (HTTP 422, machine-readable):**

| `error` | Meaning |
|---|---|
| `delegation_not_allowed` | The owner has not allowed this `tokenIn` for your address, or removed/revoked it. |
| `delegation_token_out_not_allowed` | `tokenOut` is neither an allowed token nor the network coin. |
| `delegation_cap_exceeded` | Today's UTC budget for `tokenIn` does not cover the amount; `details.resetsAt` is when it resets. |
| `delegation_expired` | The owner's expiry date has passed. |
| `delegation_allowance_low` | The owner's ERC-20 allowance to the contract is below the amount; the owner raises it by allowing the token again. |
| `insufficient_balance` | The owner does not hold the amount. |
| `delegation_unavailable` (400) | No delegation contract on this chain. The networks that have one are listed in the table above and in `GET /chains` (`delegation`). |

The same checks are enforced on-chain; if you call the contract directly, the revert reasons are `token in not allowed`, `token out not allowed`, `daily cap exceeded`, `limit expired`, `router not allowed`, `out != WETH`, or an ERC-20 `transferFrom` failure (allowance or balance).

The counter is per token and per UTC calendar day; lowering a limit mid-day keeps what was already spent. Events `LimitSet`, `Revoked` and `Swapped` are emitted for every change, so you can watch them instead of polling.

## MCP server

`@hopivia/mcp` exposes every endpoint above as tools for Claude, Cursor, Windsurf and any other [MCP](https://modelcontextprotocol.io) client. The server runs on your machine, talks to this API over HTTPS and never sees a key or a wallet: the write tools return the same unsigned transactions as `POST /swap/tx` and `POST /bridge/tx`.

Claude Code:

```bash
claude mcp add hopivia -- npx -y @hopivia/mcp
```

Claude Desktop, Cursor, Windsurf — `mcpServers` in the client's config (`claude_desktop_config.json`, `.cursor/mcp.json`, …):

```json
{
  "mcpServers": {
    "hopivia": {
      "command": "npx",
      "args": ["-y", "@hopivia/mcp"],
      "env": { "HOPIVIA_API_KEY": "hk_…" }
    }
  }
}
```

Tools: `list_chains`, `list_tokens`, `get_quote`, `build_swap_transactions` (with `onBehalfOf` for [delegated swaps](#delegated-access)), `get_bridge_quote`, `build_bridge_transactions`, `get_bridge_status`, `check_token`, `get_balances`, `get_delegation`, `api_key_info`, and for [local wallets](#local-wallets) `list_wallets`, `send_tokens`, `send_tokens_batch`, `swap_tokens`. The resource `hopivia://docs` is this guide. `HOPIVIA_API_KEY` is optional — without it the server runs at the anonymous limit. API refusals (422) come back as tool errors with their `error` and `message`, so the agent can explain and fix the request. Source and development notes: `packages/mcp` in the [API repository](https://github.com/gyuikyh/dee-back).

### Local wallets

"Send 50 USDT from work to mom": the server can also sign with a separate wallet you fund for your AI assistant (any MCP client — Claude Desktop, Claude Code, OpenAI Codex, Cursor and others). Run `npx -y @hopivia/mcp setup` — it creates or imports the wallet (on macOS the key goes to the Keychain), sets the limits per transfer and per 24 hours, the default network, the contacts and when to ask you (`always`, only for `new` addresses, `never` within the limits), and connects Claude Desktop (on Linux, Claude Code and optionally Cursor). Other MCP clients are added by hand, e.g. Claude Code: `claude mcp add hopivia -s user -- npx -y @hopivia/mcp`; OpenAI Codex: `[mcp_servers.hopivia]` with `command = "npx"` and `args = ["-y", "@hopivia/mcp"]` in `~/.codex/config.toml`; Cursor, Windsurf and others: the usual `mcpServers` JSON with the same command. The key never leaves the machine and is never sent to this API. Before signing, the server decodes every transaction the API prepared and checks the recipient, the amount, the fee wallet, a 2% fee ceiling and the pinned contract addresses; anything else is refused. Every transfer pays a visible $0.01 service fee (see [Fees](#fees)); results state the total fees in USD. `send_tokens` takes an optional `memo` (the user's comment, shown in the approval window). `send_tokens_batch` pays a whole list in one transaction with one signature where the transfer contract is deployed (`POST /transfer/batch/tx`; the server checks the signed list equals the approved one), otherwise one by one.

### Remote connector (assistant on the phone)

Mobile AI apps cannot run a local server, so there is a hosted one: `https://hopivia.com/api/v1/mcp` (Streamable HTTP, stateless, no sign-in). Add it in any assistant that can add a custom connector (remote MCP server) by URL, if your plan allows it — for example in Claude: claude.ai → Settings → Connectors → Add custom connector; it then appears in the Claude mobile app. It holds no keys and sends nothing: besides the read-only tools (`list_chains`, `list_tokens`, `get_quote`, `get_bridge_quote`, `get_bridge_status`, `check_token`, `get_balances`, `get_delegation`) it has `transfer_link`, which returns a link to `https://hopivia.com/pay?chain=…&token=…&to=…&amount=…&fees=ontop|included[&memo=…]` (`memo`: an optional comment, URL-encoded, shown on the page and signed with the transfer). The user opens it, connects their own wallet, checks the recipient and signs.

## API keys and limits

Every endpoint works without a key. A key raises the rate limit and lets you see your usage.

| Tier | Requests per minute | How to get it |
|---|---|---|
| Anonymous | 30 per IP | nothing to do |
| Free | 120 per key | [https://hopivia.com/agents](https://hopivia.com/agents): connect an EVM wallet or TronLink, sign a message (no gas), create a key |
| Pro | 600 per key | on request from the Hopivia team |

Send the key in a header:

```bash
curl "https://hopivia.com/api/v1/keys/me" -H "x-api-key: hk_…"
```

```json
{ "tier": "free", "limitPerMinute": 120,
  "key": { "prefix": "hk_yDZR90mR", "label": "my-agent", "requestsToday": 37, "requests7d": 412 } }
```

- The full key is shown **once**, when it is created. Hopivia stores only its hash.
- A wallet can hold up to 5 active keys. Revoke a key on the same page; it stops working within a minute.
- An unknown or revoked key returns `401 invalid_api_key`. It is never silently downgraded to anonymous.
- Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. A `429` carries `Retry-After` in seconds.

## Conventions

**Networks.** Pass the chain id or a key: `bsc` (56), `ethereum` (1), `polygon` (137), `arbitrum` (42161), `base` (8453), `optimism` (10), `avalanche` (43114), `opbnb` (204, bridges only). `tron` (728126428) is accepted only by the bridge endpoints.

**Tokens.** Three forms are accepted:
- `native` or the native symbol (`BNB`, `ETH`, `POL`, `AVAX`) for the network's coin;
- a symbol from `GET /tokens`, for example `USDT`, `USDC`;
- any `0x` address. Symbol, name and decimals are read from the contract.

On Tron only `USDT` is supported.

**Amounts.** Send `amount` in human units (`"0.5"`) or `amountWei` as an integer string in the smallest unit (`"500000000000000000"`). Every amount in a response comes in both forms:

```json
{ "wei": "500000000000000000", "formatted": "0.5" }
```

**Slippage.** `slippageBps` in basis points: `50` is 0.5% (default), the maximum is `1000` (10%). `minOut` is derived from it.

**Addresses.** EVM addresses are returned checksummed. Tron addresses are base58 (`T…`).

## Endpoints

### GET /quote

The best same-chain price across every exchange of the network. Read-only.

| Parameter | Required | Description |
|---|---|---|
| `chain` | yes | network id or key |
| `tokenIn`, `tokenOut` | yes | token references, see Conventions |
| `amount` or `amountWei` | yes | how much `tokenIn` to sell |
| `slippageBps` | no | default 50 |

Response fields you will use:

| Field | Meaning |
|---|---|
| `best.venue`, `best.label` | exchange and human-readable route |
| `best.amountOut`, `best.minOut` | expected and guaranteed minimum output |
| `best.feeBps`, `best.viaAggregator` | Hopivia fee on this route, see [Fees](#fees) |
| `alternatives[]` | other exchanges, best first |
| `venuesCompared` | how many exchanges were asked |
| `priceImpactPct` | how much this trade moves the price |
| `savingsPct` | best compared with second best |
| `warnings[]` | human-readable notes, for example high price impact |

### POST /swap/tx

Unsigned transactions for the best same-chain swap from a given wallet.

```json
{
  "chain": "arbitrum",
  "tokenIn": "ETH",
  "tokenOut": "USDC",
  "amount": "0.05",
  "from": "0xYourAgentWallet",
  "recipient": "0xOptionalOtherAddress",
  "slippageBps": 50,
  "maxPriceImpactPct": 10,
  "simulate": true,
  "checkSafety": true,
  "allowRiskyToken": false
}
```

| Field | Default | Description |
|---|---|---|
| `from` | required | the wallet that signs and pays |
| `recipient` | `from` | who receives `tokenOut` |
| `maxPriceImpactPct` | 10 | refuse above this, see refusals |
| `simulate` | true | dry-run the swap with `eth_estimateGas` |
| `checkSafety` | true | GoPlus check of `tokenOut` |
| `allowRiskyToken` | false | build even if `tokenOut` is high risk; only with the user's explicit consent |

The response contains `steps[]` with `kind` set to `approve`, `permit2-approve` or `swap`. Each step has `to`, `data`, `value` in wei and `chainId`. It also contains `spender`, `minOut`, `deadline` (unix seconds; V2 swaps revert after it), `simulation`, `safety` and `warnings`.

Send the steps one after another from `from`, and wait for each to be mined. Do not change `to`, `data` or `value`.

### GET /bridge/quote

A cross-chain quote. The provider is chosen by direction:

| Direction | Provider | Notes |
|---|---|---|
| Tron ↔ any EVM network | Symbiosis and deBridge DLN, quoted together — the one that delivers more is returned (`provider` in the response) | USDT only on the Tron side |
| EVM ↔ EVM | LI.FI | falls back to Symbiosis when LI.FI has no route |

| Parameter | Required | Description |
|---|---|---|
| `fromChain`, `toChain` | yes | network ids or keys, `tron` allowed |
| `fromToken`, `toToken` | yes | token references |
| `amount` or `amountWei` | yes | amount of `fromToken` |
| `from` | recommended | sender on `fromChain` (`0x…` or `T…`) |
| `to` | when formats differ | recipient on `toChain`; required for Tron ↔ EVM |
| `slippageBps` | no | default 50 |
| `provider` | no | force `lifi` or `symbiosis` |

```json
{
  "provider": "symbiosis",
  "amountIn":     { "formatted": "100" },
  "amountOut":    { "formatted": "99.284676421224127265" },
  "amountOutMin": { "formatted": "98.787754007547489072" },
  "estimatedSeconds": 32,
  "feesUsd": 0.5,
  "approveTo": "TPuaJ6gnYfE9gLUDFrbWbPx3tMnjG8max1",
  "warnings": ["Tron side: the sender needs ~70–220k Energy (or ~15–25 TRX burned) …"]
}
```

Bridging from BNB Chain to Tron costs about $4 in fixed fees. Small amounts lose a large share, and the quote warns when fees exceed 10% of the amount.

### POST /bridge/tx

The same parameters as `/bridge/quote` in a JSON body, with `from` required. It returns `steps[]` and a `track` object.

**EVM steps** (`approve`, `bridge`) have `to`, `data`, `value` like swap steps.

**Tron steps** (`tron-approve`, `tron-call`) carry what TronWeb needs:

```json
{
  "kind": "tron-call",
  "contract": "TPuaJ6gnYfE9gLUDFrbWbPx3tMnjG8max1",
  "functionSelector": "metaRoute((bytes,bytes,address[],address,address,uint256,bool,address,bytes))",
  "rawParameter": "0000…",
  "callValueSun": 0,
  "feeLimitSun": 200000000
}
```

```js
// TronWeb (tronweb npm package or the TronLink extension)
const { transaction } = await tronWeb.transactionBuilder.triggerSmartContract(
  step.contract,
  step.functionSelector,
  { feeLimit: step.feeLimitSun, callValue: step.callValueSun, rawParameter: step.rawParameter },
  [],
  fromAddress,
);
const signed = await tronWeb.trx.sign(transaction);
const result = await tronWeb.trx.sendRawTransaction(signed);
```

`track.statusUrl` is a ready-made link to `/bridge/status`; append `&txHash=` with the hash of the `bridge` or `tron-call` step.

### GET /bridge/status

| Parameter | Description |
|---|---|
| `provider` | `lifi` or `symbiosis`, from `track.provider` |
| `txHash` | hash of the source transaction |
| `fromChain`, `toChain` | as in the quote |
| `bridge` | LI.FI only: `track.bridge` |

`status` is one of:

| Status | Meaning | What to do |
|---|---|---|
| `NOT_FOUND` | not indexed yet | normal for the first minute; poll every 15–30 s |
| `PENDING` | on the way | keep polling |
| `DONE` | delivered; `receivingTxHash` is set | finish |
| `FAILED` | the bridge failed | report to the user with `explorer` |
| `STUCK` | Symbiosis could not complete | the user can revert it at app.symbiosis.finance |

### GET /token/check

A safety verdict for a token, from GoPlus Security.

```bash
curl "https://hopivia.com/api/v1/token/check?chain=bsc&address=0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82"
```

```json
{
  "token": { "symbol": "CAKE", "decimals": 18 },
  "safety": { "checked": true, "risk": "medium", "flags": ["owner can mint"],
              "isHoneypot": false, "isOpenSource": true, "holderCount": 1912641 }
}
```

| `risk` | Meaning |
|---|---|
| `low` | no red flags found |
| `medium` | owner privileges or unverified code; common in legitimate tokens, mention it to the user |
| `high` | honeypot, sell tax of 50% or more, or the owner can change balances; do not buy |
| `unknown` | GoPlus has no data; treat new tokens with care |

### GET /balances

The native coin and every known token with a non-zero balance. Add more tokens with `tokens=0x…,0x…`.

```bash
curl "https://hopivia.com/api/v1/balances?chain=bsc&address=0xYourWallet"
```

### POST /transfer/tx

A plain token transfer from `from` to `to`, with the Hopivia transfer fee ($0.01). Body: `chain`, `token`, `amount` (or `amountWei`), `from`, `to`, optional `feesIncluded` (the wallet spends `amount` in total and the recipient gets it minus the fees; default: the recipient gets exactly `amount` and fees come on top), optional `memo` — a comment in the user's own words, at most 100 bytes of UTF-8 (about 50 Cyrillic or 100 Latin characters), one line, no control or text-direction characters. The memo is signed with the transfer and recorded on-chain (the `Transferred` event), so only `contract` mode carries it; for the chain coin and on networks without the contract the request is refused with `memo_unsupported` rather than sent without it.

```bash
curl -X POST https://hopivia.com/api/v1/transfer/tx -H 'content-type: application/json' \
  -d '{"chain":"bsc","token":"USDT","amount":"50","from":"0xYourWallet","to":"0xRecipient"}'
```

`mode` says how it is sent. `contract`: on networks with the Hopivia transfer contract, an ERC-20 transfer is one transaction — send the `steps` (a one-time `approve` to Permit2 at most), then sign `permit.typedData` with `from` and call `transferWithPermit(owner, token, to, amount, maxFee, relayerFee, memo, nonce, deadline, signature)` on `permit.contract`; the signature (witness `Transfer(address to,uint256 amount,uint256 maxFee,uint256 relayerFee,string memo)`) names the recipient, the amount, a fee ceiling and the comment, so nothing else can be taken. `two-step`: a transfer step, then a separate fee step. `cost.totalUsd` is the expected network gas plus the service fee. Refusals: `insufficient_balance`, `insufficient_gas` (the message says how much of the chain coin to add), `amount_too_small`, `same_address`, `recipient_is_token`, `invalid_memo`, `memo_unsupported`, and 503 `transfers_paused` while the transfer contract is paused by its guardian (the API does not silently switch to the two-step mode then).

### POST /transfer/batch/tx

A list of recipients paid in **one** transaction through the transfer contract: everyone is paid or the transaction reverts and nobody is. Body: `chain`, `token`, `from`, `recipients` — 1 to 200 items `{to, amount}` (or `amountWei`), no duplicates, not `from`, not the token contract, not the zero address — and optional `memo` (one comment for the whole list, same rules as above).

```bash
curl -X POST https://hopivia.com/api/v1/transfer/batch/tx -H 'content-type: application/json' \
  -d '{"chain":"bsc","token":"USDT","from":"0xYourWallet","recipients":[{"to":"0xAlice","amount":"50"},{"to":"0xBob","amount":"20"}],"memo":"salary"}'
```

ERC-20: send the `steps` (a one-time `approve` to Permit2 at most), sign `permit.typedData` (witness `BatchTransfer(Recipient[] recipients,uint256 maxFee,uint256 relayerFee,string memo)`, `Recipient(address to,uint256 amount)`) with `from` and call `transferBatchWithPermit(owner, token, recipients, maxFee, relayerFee, memo, nonce, deadline, signature)` on `permit.contract` with the values from `permit.call`. One signature covers the whole list in its order. The chain coin: one payable `transferNativeBatch(recipients, maxFee, memo)` step whose `value` is the total plus the fee ceiling; the contract refunds what it does not charge. The fee is $0.01 per recipient (`fee.amount`), `fee.maxFee` is the signed ceiling of the total (+20% for price moves); `cost` has `totalUsd`, `networkUsd`, `serviceUsd`. Refusals: `insufficient_balance`, `insufficient_gas`, `duplicate_recipient`, `same_address`, `recipient_is_token`, `invalid_memo`; 409 `batch_unavailable` when the network has no transfer contract or the contract cannot price the token — send the transfers one by one with `POST /transfer/tx` then; 503 `transfers_paused` while the contract is paused.

### GET /transfer/estimate

The fees of a transfer before the sender is known: `chain`, `token`, `amount`, optional `feesIncluded` → `recipientGets`, `walletSpends`, `feeUsd`, `networkUsd`, `totalUsd`, `mode`. Balances are not checked.

### GET /price

USD price of a token: `/price?chain=bsc&token=CAKE`. Stablecoins are 1.

### GET /delegation

What a wallet owner allowed your agent on the delegation contract. Version 1: per token the daily cap, what is spent and available today, the expiry and the owner's allowance. [Version 2](#delegation-v2): one dollar limit (`grant`), the sell and buy lists and the address book. Plus the contract address and its routers. Read-only; see [Delegated access](#delegated-access) for the swap itself (`POST /swap/tx` with `onBehalfOf`).

```bash
curl "https://hopivia.com/api/v1/delegation?chain=bsc&owner=0xOwnerWallet&agent=0xYourAgentWallet"
```

### GET /chains

Networks with `swap` and `bridge` flags, the number of exchanges per network, whether the Hopivia aggregator is deployed there and which delegation version is live (`venues.delegation`, `venues.delegationV2`).

### GET /tokens

Known tokens of a network: `/tokens?chain=bsc&q=usd`. Any other token can still be used by its address.

### GET /health

`{ "ok": true, "version": "0.1.0" }` — for uptime checks. Not rate limited.

## Errors and refusals

Every error has the same shape. Branch on `error`, which is a stable code; `message` is for people.

```json
{
  "error": "price_impact_too_high",
  "message": "price impact 0.09% exceeds maxPriceImpactPct=0.01; reduce the amount or raise the ceiling explicitly",
  "details": { "priceImpactPct": 0.09, "maxPriceImpactPct": 0.01 }
}
```

`POST /swap/tx` **refuses** to return transactions that should not be sent. That is the difference from APIs that only warn.

| HTTP | `error` | What the agent should do |
|---|---|---|
| 422 | `simulation_reverted` | the swap would fail from this wallet; read `details.simulation.error`, do not send |
| 422 | `price_impact_too_high` | split the order or ask the user before raising `maxPriceImpactPct` |
| 422 | `token_risk_high` | tell the user why (`details.flags`); retry with `allowRiskyToken` only after an explicit yes |
| 422 | `insufficient_balance` | the wallet holds less than `amount`; `details` has both values |
| 422 | `insufficient_gas` | `POST /transfer/tx`: the wallet lacks the chain coin for gas; the message says how much to add |
| 400 | `validation`, `invalid_amount`, `invalid_address`, `unknown_token`, `unsupported_chain`, `same_token`, `amount_too_low`, `amount_too_small`, `same_address`, `recipient_is_token`, `duplicate_recipient`, `invalid_memo` | fix the request |
| 400 | `memo_unsupported` | this transfer cannot carry a comment (the chain coin, or no transfer contract on the network); ask the user whether to send without it |
| 409 | `batch_unavailable` | `POST /transfer/batch/tx`: no transfer contract here, or it cannot price the token; send one by one with `POST /transfer/tx` |
| 503 | `transfers_paused` | the transfer contract is paused by its guardian; tell the user, retry later; nothing was built |
| 401 | `invalid_api_key` | remove or replace the key |
| 404 | `no_route` | no liquidity for this pair or size |
| 429 | `rate_limited` | wait `Retry-After` seconds |
| 502 | `upstream_error`, `provider_error`, `rpc_error` | a node or bridge provider failed; retry with backoff |

## Fees

- **Same-chain swaps.** On networks where the Hopivia aggregator contract is deployed (`GET /chains` → `aggregator`), every swap goes through it, V2 and V3 venues alike, and it keeps 0.15% of the input before the swap. Elsewhere the exchange router is called directly and Hopivia charges nothing. Every quote states `feeBps` and `viaAggregator`, so the fee is always visible before signing, and `amountOut` is already net of it. Exchange pool fees are included in `amountOut` too.
- **Transfers.** `POST /transfer/tx` adds a fixed $0.01 per transfer (`POST /transfer/batch/tx`: $0.01 per recipient), paid to the Hopivia fee wallet in the same token (in the chain coin when the token has no price). On networks with the transfer contract the fee is set in the contract (changes only after a 48-hour timelock, at most $0.10) and goes in the same transaction. Network gas is extra and goes to the network.
- **Cross-chain.** `feesUsd` includes the bridge provider's fees; `gasUsd` is the source-chain gas when the provider reports it.
- **The API itself** is free within the limits above.

## Rules for AI agents

Put this in your agent's instructions:

```text
You can trade through the Hopivia API (docs: https://hopivia.com/docs.md).
1. Before any trade, call GET /quote or GET /bridge/quote and tell the user the expected output,
   the minimum output, the price impact and the fees. Get their confirmation.
2. Build transactions only with POST /swap/tx or POST /bridge/tx, passing your own wallet as `from`.
   Never change `to`, `data` or `value` of a returned step.
3. Send steps strictly in order and wait for each receipt. After an approve is mined, request /swap/tx again.
4. If the API refuses (HTTP 422), do not try to work around it. Explain the `error` to the user.
   Use `allowRiskyToken` only after the user explicitly agrees to buy a high-risk token.
5. Never send a private key or seed phrase to any API, including this one.
6. For Tron ↔ EVM always pass `to` in the destination network's format and confirm it with the user.
7. After a bridge transaction, poll GET /bridge/status every 15–30 s until DONE or FAILED.
8. With delegated access (see "Delegated access"), trade only within limitOf() of the user's wallet and report
   every trade: what you sold, what arrived at the user's address and at which price.
```

## Managing keys from code

The [agents page](https://hopivia.com/agents) uses these endpoints. You can call them yourself, for example to rotate keys automatically. They accept EVM wallets (`personal_sign`) and Tron wallets (`tronWeb.trx.signMessageV2`, as TronLink does); smart-contract wallets are not supported yet.

1. `POST /keys/challenge` with `{"address": "0x…"}` or `{"address": "T…"}` returns a `message` and `nonce`. The message is valid for 10 minutes.
2. Sign `message`: `personal_sign` for EVM, `signMessageV2` for Tron. This is an off-chain signature with no gas.
3. `POST /keys/session` with `{"address", "nonce", "signature"}` returns a `session` valid for 12 hours.
4. With `Authorization: Bearer <session>`:
   - `GET /keys` lists keys with usage;
   - `POST /keys` with `{"label": "my-agent"}` creates a free key and returns the raw key once;
   - `DELETE /keys/{id}` revokes a key.

```js
const { message, nonce } = await post('/keys/challenge', { address: account.address });
const signature = await account.signMessage({ message });
const { session } = await post('/keys/session', { address: account.address, nonce, signature });
const { key } = await post('/keys', { label: 'trading-agent' }, { authorization: `Bearer ${session}` });
```

## Versioning

Version 1 of the API is stable: fields may be added, but existing fields and error codes will not change meaning. Breaking changes will go to `/api/v2`.
