Hopivia API
Hopivia gives AI agents, trading bots and scripts the same thing the web app 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:
- Quick start — quote, build, sign from the agent's own wallet.
- 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.
- MCP server — one command, and Hopivia becomes a tool in Claude, Cursor and other assistants.
| Base URL | https://hopivia.com/api/v1 |
| Format | JSON, UTF-8 |
| Auth | optional X-API-Key header — get a free key |
| Limit instead of a key | https://hopivia.com/agents — the user sets it there, see Delegated access |
| This guide as Markdown | https://hopivia.com/docs.md — give it to your model |
| OpenAPI 3.1 | https://hopivia.com/api/openapi.json · interactive reference |
| Discovery | /llms.txt · /.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.
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:
{
"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.
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"}'{
"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
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'));#Delegated access
A user can give your agent a limit instead of a key. On the agents page 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 executes through the Hopivia aggregator with its 0.15 % fee.
| Network | Chain ID | Contract | Exchanges |
|---|---|---|---|
| Ethereum | 1 | 0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0 | 3 |
| Optimism | 10 | 0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0 | 2 |
| BNB Chain | 56 | 0xeB2028f91A53d65F199fB90aAb4701C72bdC0E0f | 7 |
| Polygon | 137 | 0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0 | 4 |
| Base | 8453 | 0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0 | 4 |
| Arbitrum | 42161 | 0xE2a1dA5eaBA7fAb6c803005B8A6Fc900a4c900E0 | 3 |
| Avalanche | 43114 | 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
amountOutandamountOutMin.
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):
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 callTrading flow through the API (the API never signs; it checks the limits and builds the one contract call):
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.POST /swap/txwithfrom= your wallet andonBehalfOf= the owner. The API quotes only on the contract's routers, verifies the limits and returns a singleswapstep on the delegation contract.tokenInmust be an ERC-20 the owner allowed (the contract cannot pull the network coin, so the owner allows WBNB/WETH to sell BNB/ETH);tokenOutmust be an allowed token ornative. No Hopivia fee on delegated swaps.- 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 — anddelegated.resetsAt.
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 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:
claude mcp add hopivia -- npx -y @hopivia/mcpClaude Desktop, Cursor, Windsurf — mcpServers in the client's config (claude_desktop_config.json, .cursor/mcp.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), get_bridge_quote, build_bridge_transactions, get_bridge_status, check_token, get_balances, get_delegation, api_key_info, and for 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.
#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); 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: 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:
curl "https://hopivia.com/api/v1/keys/me" -H "x-api-key: hk_…"{ "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-LimitandX-RateLimit-Remaining. A429carriesRetry-Afterin 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:
nativeor the native symbol (BNB,ETH,POL,AVAX) for the network's coin;- a symbol from
GET /tokens, for exampleUSDT,USDC; - any
0xaddress. 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:
{ "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 |
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.
{
"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 |
{
"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:
{
"kind": "tron-call",
"contract": "TPuaJ6gnYfE9gLUDFrbWbPx3tMnjG8max1",
"functionSelector": "metaRoute((bytes,bytes,address[],address,address,uint256,bool,address,bytes))",
"rawParameter": "0000…",
"callValueSun": 0,
"feeLimitSun": 200000000
}// 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.
curl "https://hopivia.com/api/v1/token/check?chain=bsc&address=0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82"{
"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….
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.
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).
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: 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 for the swap itself (POST /swap/tx with onBehalfOf).
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.
{
"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 statesfeeBpsandviaAggregator, so the fee is always visible before signing, andamountOutis already net of it. Exchange pool fees are included inamountOuttoo. - Transfers.
POST /transfer/txadds 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.
feesUsdincludes the bridge provider's fees;gasUsdis 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:
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 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.
POST /keys/challengewith{"address": "0x…"}or{"address": "T…"}returns amessageandnonce. The message is valid for 10 minutes.- Sign
message:personal_signfor EVM,signMessageV2for Tron. This is an off-chain signature with no gas. POST /keys/sessionwith{"address", "nonce", "signature"}returns asessionvalid for 12 hours.- With
Authorization: Bearer <session>:GET /keyslists keys with usage;POST /keyswith{"label": "my-agent"}creates a free key and returns the raw key once;DELETE /keys/{id}revokes a key.
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.