Docs · API reference · v1
Intent Parser API.
A single endpoint that turns natural-language swap descriptions into structured order intents. Free, rate-limited at 30 requests/min per IP, no auth, no key required. Build agents, Telegram/Discord bots, browser extensions, or your own UIs on top of Ophis's intent layer.
Endpoint
Runs on Cloudflare Pages Functions at the edge. Upstream model is LibertAI Qwen 3.5 122B. Response is structured JSON — no streaming, no SSE, single request/response with a 5-second upstream timeout.
Headers
| Header | Required | Notes |
|---|---|---|
content-type | Yes | application/json |
origin | Browser only | If present, must match the allow-list (see Origins). Server-side callers (no Origin header) are allowed. |
Request
{
"text": "swap 100 USDC for ETH on Base"
}
| Field | Type | Constraints |
|---|---|---|
text | string | Required. Non-empty. Max 280 chars. |
Response — success
{
"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 }
]
}
}
The model is allowed to emit any DEX-traded symbol it recognises; entities
whose value isn't in the server-side allow-list (236 tokens / 14 chains)
are silently filtered out before the response. The top-level shape is the
contract — extra entities never appear.
ParsedIntent
| Field | Type | Notes |
|---|---|---|
intent | "swap" | "unknown" | "unknown" means the input wasn't recognised as a swap. Always returns 200, not an error. |
entities | Entity[] | Possibly empty. Order matches input text-position. |
Entity
| Field | Type | Notes |
|---|---|---|
type | "sellToken" | "buyToken" | "amount" | "chain" | Entity role. |
value | string | Canonical form. Uppercase symbol for tokens (USDC, WETH, PEPE), lowercase slug for chains (ethereum, base, arbitrum), decimal numeric string for amounts. |
raw | string | Exact substring from the input that matched. |
start | int | 0-indexed character offset in the input. Inclusive. |
end | int | 0-indexed character offset. Exclusive. text.slice(start, end) === raw. |
Response — error
{
"ok": false,
"error": {
"code": "RATE_LIMITED",
"message": "too many requests"
}
}
All error responses share this shape. HTTP status reflects the error class
(400/403/429/500/502/504). The message is human-readable and may
change between deploys — clients should branch on code, not on text.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
BAD_INPUT | 400 | Missing text, empty text, text over 280 chars, or invalid JSON body. |
FORBIDDEN | 403 | Browser Origin header present but not on the allow-list. |
RATE_LIMITED | 429 | 30/min per IP exceeded. Response includes a Retry-After header in seconds. |
UPSTREAM | 500 / 502 | LibertAI returned non-2xx, or the function isn't configured (LIBERTAI_API_KEY missing). |
TIMEOUT | 504 | LibertAI didn't respond within 5 seconds. |
INVALID_JSON | 502 | LibertAI returned content that didn't parse as the expected schema. Rare; usually transient. |
Rate limits
30 requests / minute / IP. Sliding window, KV-backed
(distributed across all Cloudflare edge isolates). Counts every request,
including ones that return FORBIDDEN or BAD_INPUT.
When you hit the limit, you get a 429 with a Retry-After header.
Back off and retry after that many seconds. If you need higher
throughput for a legitimate integration, reach out — the function is
open-source and we'll happily document a self-hosting path.
Implementation note: cf-connecting-ip is the rate-limit key.
Multiple users behind a single egress IP (corporate NAT, mobile carriers,
etc.) share the bucket. Edge cases produce predictable but unwelcome 429s
— server-side proxies are the workaround.
Origin allow-list
Browser callers (those that set an Origin header) must originate from:
https://ophis.fi*.greg-etm.pages.dev(Cloudflare Pages preview deploys)*.greg.pages.dev(preview deploys after the Pages project rename)
Server-side callers (curl, Node fetch, Python requests, anything that
doesn't set Origin) are not blocked. This is
not a security boundary — Origin is spoofable by anything that
isn't a browser — just a raised bar for casual scraping from third-party
web UIs.
Supported values
The server filters the model's output against a hard allow-list. Entities
with value outside the list are dropped before the response leaves the
edge.
Chains (14)
ethereum · optimism · base · arbitrum · polygon · avalanche · gnosis · linea · bnb · megaeth · scroll · blast · mantle · zksync
Tokens (~236)
Includes stablecoins, ETH-pegs + LSTs + LRTs, BTC-pegs, native L1/L2
symbols, DeFi blue-chips, AI / DePIN / RWA tokens, memes, and gaming.
Full list lives in the source at
functions/api/intent.ts
under TOKEN_VALUES. New additions land via PR when sustained
user demand justifies them.
Examples
curl
curl -sS https://ophis.fi/api/intent \
-H 'content-type: application/json' \
-d '{"text":"swap 100 USDC for ETH on Base"}' | jq
fetch (JavaScript / TypeScript)
interface ParsedIntent {
intent: 'swap' | 'unknown';
entities: Array<{
type: 'sellToken' | 'buyToken' | 'amount' | 'chain';
value: string;
raw: string;
start: number;
end: number;
}>;
}
type IntentResponse =
| { ok: true; data: ParsedIntent }
| { ok: false; error: { code: string; message: string } };
async function parseIntent(text: string): Promise<ParsedIntent> {
const res = await fetch('https://ophis.fi/api/intent', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text }),
});
const body = (await res.json()) as IntentResponse;
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.data;
}
// Usage
const parsed = await parseIntent('swap 100 USDC for ETH on Base');
console.log(parsed.entities);
Python
import requests
resp = requests.post(
'https://ophis.fi/api/intent',
json={'text': 'swap 100 USDC for ETH on Base'},
timeout=10,
)
body = resp.json()
if not body['ok']:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
for entity in body['data']['entities']:
print(entity['type'], '=', entity['value'])
Error: rate limit
HTTP/1.1 429 Too Many Requests
content-type: application/json
retry-after: 42
{"ok":false,"error":{"code":"RATE_LIMITED","message":"too many requests"}}
Edge case: non-swap input
POST /api/intent
{"text":"hello world"}
200 OK
{"ok":true,"data":{"intent":"unknown","entities":[]}}
Non-swap input returns 200 with intent: "unknown" and an
empty entities array. Not an error.
From parsed intent to an executable swap
The endpoint returns an intent; it does not place a trade. To turn a parsed intent into an actual on-chain swap you have two paths:
-
Hand off to Ophis.fi's UI — construct the deep-link
https://ophis.fi/#/<chainId>/swap/<sellToken>/<buyToken>and open it in the user's browser. The cowswap form pre-populates and the user signs from their own wallet. Chain ID mapping: chainMap.ts. -
Build the order yourself — use
the underlying protocol's SDK
with the entity values resolved to ERC-20 addresses (token-list of your
choice). Sign with the user's wallet, post to
api.cow.fi/<chain>/v1/orders. SetappData.metadata.appCode = "ophis"if you want the order to be attributed to Ophis for analytics and partner-fee accounting; the recipient Safe is documented in our public spec.
Stability & versioning
The shape of ParsedIntent, Entity, the error-code
enum, and the rate-limit policy are stable for v1. We
will not remove fields or rename error codes without first publishing a
deprecation note here and at least 30 days of overlap. Additive changes
(new entity types, new error codes, additional response fields) are
possible without notice — code defensively.
The underlying model is currently qwen3.5-122b-a10b via
LibertAI; this is an implementation detail and may rotate. Latency,
timeout, and rate-limit guarantees are part of the v1 contract; the
specific model is not.
Contact
Found a parsing bug, want a token added, hitting an unhelpful 429? Open an issue at github.com/ophis-fi/ophis/issues.