Skip to main content

AI agent integration

Ophis is designed to be agent-friendly. The Intent API accepts free-form natural language and returns structured JSON your agent can map directly to a pre-filled swap link. The agent does the parsing and routing; the human always reviews and signs.

New: the full walkthrough

For a narrative guide, including the MEV and key-safety pitfalls of letting an agent trade, read How to let an AI agent swap tokens on the Ophis blog.

The fastest way to give an MCP-capable agent full Ophis access is the hosted Model Context Protocol server:

https://mcp.ophis.fi/mcp

It speaks streamable-HTTP MCP and exposes twelve tools:

ToolWhat it does
parse_intentParse a natural-language request into a structured intent.
resolve_tokenResolve a token symbol to its canonical address from the trusted Ophis/CoW token list; fails closed (anti-spoof). Call this before quoting or building so you never trade against a spoofed address.
get_quoteFetch an executable quote for a parsed intent.
build_orderBuild a bounded, ready-to-sign order (receiver pinned to the owner by default).
submit_orderSubmit a signed order to the correct per-chain orderbook.
lookup_tierLook up a wallet's 30-day volume tier / rebate status.
list_chainsResolve supported chains and their settlement / orderbook hosts.
get_balancesRead a wallet's native and ERC-20 balances on one chain via a public RPC.
get_portfolioRead a wallet's token balances across multiple chains.
get_gasFetch the current gas price for a chain.
get_token_chartFetch a token's OHLCV price chart.
expected_surplusEstimate how much better an Ophis sell-quote beats the open market (beatBps).

Point any MCP client (Claude, Cursor, or a custom agent) at that URL. Ophis never holds keys: build_order returns a bounded order the agent signs locally; the signature is the trust boundary (see the warning below). A bare request without an Accept: text/event-stream header returns HTTP 406; that is the transport negotiating, not an outage.

Connect your MCP client

The server is public and keyless, so there is nothing to sign up for. Copy the block for your client.

Claude Code (one command):

claude mcp add --transport http ophis https://mcp.ophis.fi/mcp

Claude Desktop (claude_desktop_config.json, via the mcp-remote bridge):

{
"mcpServers": {
"ophis": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.ophis.fi/mcp"]
}
}
}

Cursor (~/.cursor/mcp.json, or the project .cursor/mcp.json):

{
"mcpServers": {
"ophis": {
"url": "https://mcp.ophis.fi/mcp"
}
}
}

VS Code (.vscode/mcp.json):

{
"servers": {
"ophis": {
"type": "http",
"url": "https://mcp.ophis.fi/mcp"
}
}
}

OpenAI Agents SDK, LangChain, or any custom client: point a streamable-HTTP MCP transport at https://mcp.ophis.fi/mcp. The LangChain tool and function-calling examples below show the call path without an MCP client at all.

After connecting, ask the agent to "quote 100 USDC to ETH on Optimism" and it will call resolve_token, get_quote, and build_order; it returns a bounded order for you (or your wallet) to sign.

If you'd rather make a single REST call than wire up the full toolset, use the Intent API directly, as shown next.

The integration flow

  1. Parse. POST the user's request (or your agent-generated trade idea) to https://ophis.fi/api/intent.
  2. Read. Receive a ParsedIntent with normalized sellToken, buyToken, amount, and chain entities.
  3. Build a deep link. Map the chain slug to its chain ID and construct https://ophis.fi/#/<chainId>/swap/<sellToken>/<buyToken>.
  4. Hand off. Open the link for the user to review and sign. Ophis never auto-signs, every order requires explicit wallet approval.
The signature is the trust boundary

Ophis intentionally does not implement x402 or any HTTP-native payment automation. An order only becomes real when the user signs it in their wallet; bypassing that step would break self-custody. Agents must always hand off to the user for signing.

Server-side callers (no browser Origin header) are allowed, subject to the 30 req/min/IP rate limit. Honour 429 + Retry-After.

Minimal example (curl)

curl -sS https://ophis.fi/api/intent \
-H 'content-type: application/json' \
-d '{"text":"swap 100 USDC for ETH on Base"}'
{
"ok": true,
"data": {
"intent": "swap",
"entities": [
{ "type": "amount", "value": "100", "raw": "100", "start": 5, "end": 8 },
{ "type": "sellToken", "value": "USDC", "raw": "USDC", "start": 9, "end": 13 },
{ "type": "buyToken", "value": "ETH", "raw": "ETH", "start": 18, "end": 21 },
{ "type": "chain", "value": "base", "raw": "Base", "start": 25, "end": 29 }
]
}
}

Python helper

import requests

OPHIS = "https://ophis.fi"

# The 12 EVM chains the Intent API can return, mapped to their chain IDs.
# Keep in sync with the API's supported-network list; build_deeplink()
# raises on any future slug not listed here rather than misrouting it.
CHAIN_SLUG_TO_ID = {
"ethereum": 1,
"optimism": 10,
"bnb": 56,
"gnosis": 100,
"polygon": 137,
"base": 8453,
"ink": 57073,
"linea": 59144,
"arbitrum": 42161,
"avalanche": 43114,
"plasma": 9745,
"unichain": 130,
}


def parse_intent(text: str) -> dict:
"""Call the Ophis Intent API and return the ParsedIntent payload."""
resp = requests.post(f"{OPHIS}/api/intent", json={"text": text}, timeout=10)
resp.raise_for_status()
body = resp.json()
if not body["ok"]:
raise RuntimeError(f'{body["error"]["code"]}: {body["error"]["message"]}')
return body["data"]


def build_deeplink(parsed: dict) -> str:
"""Turn a ParsedIntent into a swap deep link for the user to sign."""
by_type = {e["type"]: e["value"] for e in parsed["entities"]}
sell = by_type.get("sellToken", "_")
buy = by_type.get("buyToken", "_")
chain_slug = by_type.get("chain")
if chain_slug is None:
chain_id = 1 # no chain in the request -> default to Ethereum
elif chain_slug in CHAIN_SLUG_TO_ID:
chain_id = CHAIN_SLUG_TO_ID[chain_slug]
else:
# The parser may return a chain this map doesn't cover yet. Fail
# loud instead of silently routing the user to the wrong chain.
raise ValueError(f"unmapped chain slug {chain_slug!r}; update CHAIN_SLUG_TO_ID")
# The user sets/confirms the amount and signs in the app.
return f"{OPHIS}/#/{chain_id}/swap/{sell}/{buy}"


intent = parse_intent("swap 100 USDC for ETH on Base")
print(build_deeplink(intent)) # https://ophis.fi/#/8453/swap/USDC/ETH

LangChain tool

Wrap the API as a LangChain tool your agent can call when a user wants to trade:

from langchain_core.tools import tool


@tool
def ophis_swap_intent(text: str) -> dict:
"""Parse a natural-language swap request into a structured Ophis intent
and a deep link the user can open to review and sign. Use this
whenever a user wants to swap, buy, or sell a crypto token.
The link must be shown to the user, never auto-execute a trade."""
parsed = parse_intent(text)
return {"intent": parsed, "deeplink": build_deeplink(parsed)}

The tool returns both the structured intent (so your agent can reason about the trade) and a link (so the user can sign it).

AutoGPT / function-calling agents

Any function-calling agent. AutoGPT commands, OpenAI Assistants, or a custom tool loop, can register the parser with this schema:

{
"type": "function",
"function": {
"name": "ophis_parse_intent",
"description": "Parse a natural-language crypto swap request into a structured intent (sellToken, buyToken, amount, chain). Returns a deep link the user opens to review and sign. Never auto-executes a trade.",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The swap request in natural language, e.g. 'swap 100 USDC for ETH on Base'. Max 280 characters."
}
},
"required": ["text"]
}
}
}

Implement the handler by POSTing { "text": <text> } to https://ophis.fi/api/intent (see the Python helper above), then surface the resulting deep link to the user.

Submitting orders programmatically

The Intent API only normalizes language, it does not place orders. To submit orders programmatically, build and sign a CoW Protocol order yourself. Four things must each be exactly right, every one fails silently (a rejected order, a wrong-chain trade, or zero fee collected) if you guess.

The helpers below live in @ophis/sdk, published on npm (v0.2.3, public). Install it with npm install @ophis/sdk, or copy the values from the call-outs if you prefer to vendor them.

1. Resolve the orderbook host from the chain ID

Optimism and Unichain do not live on api.cow.fi

Optimism and Unichain break the api.cow.fi/<slug> pattern. Ophis self-hosts their orderbooks at optimism-mainnet.ophis.fi and unichain-mainnet.ophis.fi. Posting one of their orders to api.cow.fi/<slug> (a host that does not serve Ophis) silently bypasses the Ophis solver and zeroes the partner fee. Resolve hosts via @ophis/sdk getOphisOrderbookUrl per chain rather than hardcoding.

import { getOphisOrderbookUrl } from '@ophis/sdk';

const orderbookUrl = getOphisOrderbookUrl(10); // -> https://optimism-mainnet.ophis.fi
// Throws on an invalid or unsupported chainId rather than guessing a host.

2. Build the partner-fee appData correctly

The partner fee for SDK and manual integrations is a flat 0.05% (5 bps) fee on trade volume (the Ophis swap app charges its own 0.10% retail rate), written into the order's appData at metadata.partnerFee. This section is for callers that build appData themselves; the keyless MCP build_order embeds this fee for you at the flat 5 bps rate (it does not apply the reduced stable-pair rate). Use the CIP-75 volume shape { volumeBps: 5, recipient }, not the price-improvement shape { priceImprovementBps, maxVolumeBps, recipient }: the two shapes use different denominators, so slotting a value into the wrong field is a silent magnitude error. Hash the appData with cow-sdk's deterministic serializer, never keccak256(JSON.stringify(doc)). JSON key order isn't stable, so the hash won't match what solvers expect.

Stablecoin-to-stablecoin swaps pay a reduced 0.01% (1 bp): same-chain pairs where both tokens are stablecoins use { volumeBps: 1, recipient } instead of { volumeBps: 5, recipient }. The @ophis/sdk exposes OPHIS_STABLE_VOLUME_FEE_BPS and a helper ophisVolumeBpsForPair(isStablePair) to pick the right rate. The SDK is chain-only and cannot detect the pair itself, so integrators pass isStablePair based on their own token classification.

import { MetadataApi, stringifyDeterministic } from '@cowprotocol/cow-sdk';
import { keccak256, toUtf8Bytes } from 'ethers';
import { buildOphisAppDataPartnerFee } from '@ophis/sdk';

// buildOphisAppDataPartnerFee(chainId) REQUIRES a chainId and THROWS on a
// missing/invalid one (a forgotten arg fails loud, not as a silent `undefined`).
// It returns the metadata.partnerFee value on every chain in the SDK's
// OPHIS_FEE_CHAIN_IDS (the Ophis-operated chains plus the CoW-hosted chains the
// fork serves), or `undefined` on any other chain.
//
// On the Ophis-operated chains (Optimism, Unichain) the partner rate you charge
// is 5 bps (1 bp stable pairs). The backend also enforces an anti-abuse MINIMUM:
// it rejects (HTTP 400) any order to the Ophis fee recipient whose fee is below
// 4 bps non-stable (or 1 bp for a same-chain stablecoin pair), or that uses a
// Surplus/PriceImprovement policy. The 4 bps is a floor the backend accepts, not
// a rate to target. Carry this fragment unchanged: do not lower the bps and do
// not drop the fee, or the order is rejected.
const partnerFee = buildOphisAppDataPartnerFee(10);
// -> the Ophis flat-volume partner-fee fragment { volumeBps: 5, recipient }
// for this chain (enforced as a minimum on Optimism: at least the floor)

const metadataApi = new MetadataApi();
const doc = await metadataApi.generateAppDataDoc({
appCode: 'ophis',
metadata: {
partnerFee,
hooks: {}, // pin empty, appData hooks are arbitrary on-chain calls
},
});
const fullAppData = await stringifyDeterministic(doc);
const appDataHash = keccak256(toUtf8Bytes(fullAppData)); // bytes32 -> order.appData

3. Sign with the correct EIP-712 domain

CoW orders are signed with EIP-712 typed data (signTypedData), never signMessage. The verifyingContract is chain-specific, and the Ophis-operated chains do not use CoW's canonical settlement.

The Optimism and Unichain settlements are not the canonical CoW one

On Optimism, Ophis's GPv2Settlement is 0x310784c7…B859, and on Unichain it is 0x108A678716e5E1776036eF044CAB7064226F714E, not the canonical 0x9008D19f…ab41. cow-sdk defaults to the canonical address, so signing an OP order with the SDK default yields a domain separator the deployed contract rejects, every order fails. Build the domain from the chain ID instead.

import { getOphisOrderDomain } from '@ophis/sdk';

// CoW's EIP-712 order struct is named `Order` (the Solidity library is
// GPv2Order, but the EIP-712 type name, which feeds the type hash, is
// `Order`; a wrong name produces a valid-looking but unusable signature).
const ORDER_TYPES = {
Order: [
{ name: 'sellToken', type: 'address' },
{ name: 'buyToken', type: 'address' },
{ name: 'receiver', type: 'address' },
{ name: 'sellAmount', type: 'uint256' },
{ name: 'buyAmount', type: 'uint256' },
{ name: 'validTo', type: 'uint32' },
{ name: 'appData', type: 'bytes32' },
{ name: 'feeAmount', type: 'uint256' },
{ name: 'kind', type: 'string' },
{ name: 'partiallyFillable', type: 'bool' },
{ name: 'sellTokenBalance', type: 'string' },
{ name: 'buyTokenBalance', type: 'string' },
],
};

// ethers v6, signer.signTypedData(domain, types, value). The domain's
// verifyingContract must be the Ophis OP settlement (getOphisOrderDomain).
const signature = await wallet.signTypedData(getOphisOrderDomain(10), ORDER_TYPES, order);
// NOT wallet.signMessage(order), that produces an invalid order signature.

4. Pin the order receiver

A CoW order's receiver is part of the signed payload and is fully caller-controlled. Pin it to the order owner: a non-owner receiver sends the bought tokens elsewhere on settlement, and the signature makes that irreversible. In the UI a wallet prompt gates this; an autonomous signer has no such gate, so guard it in code before signing.

import { assertReceiverIsOwner } from '@ophis/sdk';

assertReceiverIsOwner(owner, order.receiver); // throws if receiver !== owner

Autonomous agent trading (advanced)

Everything above keeps a human in the signing loop. For an agent that signs without human review, off-chain helpers are not enough, a compromised or prompt-injected agent will sign whatever it is told. Safety has to be enforced where the agent cannot reach it:

  1. Funds in a smart account (Safe). The agent never holds the fund-owning key; it only proposes orders. The account's EIP-1271 validator (or a Safe module) approves only order hashes that satisfy policy.
  2. A deterministic policy gate between the (untrusted) LLM and any signature, owning every order field:
    • token resolution from a chain-scoped allowlist only, never an LLM-emitted address;
    • receiver pinned to the account;
    • appData pinned to the Ophis canonical, hooks forced empty;
    • limit price within X% of an independent, staleness-checked oracle (CoW guarantees you won't fill below your limit, not that your limit is sane);
    • per-trade notional + rolling daily caps; short validTo; avoid presign.
  3. Containment: a bounded vault-relayer allowance (the blast radius if policy fails once), a guardian key that can revoke signing or pause, keys in an HSM/TEE, and a tamper-evident audit trail.
  4. Defense in depth: enforce the policy in two places, the EIP-1271 validator/signer and server-side at orderbook ingestion.
The signing gate must be in code, not prose

Today "the human always signs" is a documented social contract, not an enforced boundary. Autonomous signing is fine to pursue, but only once that promise is replaced by the policy-enforced kit above. Otherwise an autonomous integrator is one unpinned receiver away from draining itself.