AI-native CLI for Relay Protocol's API — dynamically built from the OpenAPI spec at runtime.
Relay is a cross-chain payments protocol. It enables instant bridging and swapping across 80+ blockchains (Ethereum, Base, Arbitrum, Solana, Bitcoin, and more) using a solver network that fills orders from their own inventory, then settles on-chain. The API covers quoting, request tracking, chain/currency discovery, and deposit address flows.
Inspired by Justin Poehnelt's post on rewriting CLIs for AI agents and the Google Workspace CLI, which dynamically generates its entire command surface from Google's Discovery Service. We apply the same pattern to Relay's OpenAPI spec.
- The Problem
- Quick Start
- For AI Agents
- Commands
- Chain & Token Resolution
- Output Formats
- Field Filtering
- Input Validation
- Safety Guards
- Configuration
- Examples
- Architecture
- License
Relay's API has 29 public endpoints across 75+ chains. AI agents are bad at using it:
- The chains response wraps in
{"chains": [...]}, not a bare array — agents assume the wrong shape solverAddressesis an array per chain, not a string — agents do string comparison and get nothing- The OpenAPI spec is 954KB with zero reusable schemas (everything inlined) — too large for context windows
- No schema introspection — agents guess at field names and response shapes
The existing SDK (relay-kit) is browser-focused (React hooks, wallet connection). It isn't designed for terminal workflows or AI agents.
Task: Get solver wallet addresses and chain IDs from the Chains API.
Without the CLI — raw GET /chains returns 286KB across 81 chains, each with 26 fields:
{
"chains": [
{
"id": 1,
"name": "ethereum",
"displayName": "Ethereum",
"httpRpcUrl": "https://ethereum.publicnode.com",
"wsRpcUrl": "wss://ethereum.publicnode.com",
"explorerUrl": "https://etherscan.io",
"explorerName": "Etherscan",
"depositEnabled": true,
"tokenSupport": "All",
"disabled": false,
"partialDisableLimit": 0,
"blockProductionLagging": false,
"currency": { "id": "eth", "symbol": "ETH", "decimals": 18, "..." : "..." },
"withdrawalFee": 0,
"depositFee": 0,
"surgeEnabled": false,
"featuredTokens": ["... 5 token objects with metadata/logos ..."],
"erc20Currencies": ["..."],
"solverCurrencies": ["..."],
"iconUrl": "...",
"contracts": { "..." : "..." },
"vmType": "evm",
"baseChainId": null,
"solverAddresses": ["0xf70d...", "0x56c2..."],
"tags": ["..."],
"protocol": "evm"
}
]
}An agent gets this 286KB blob, has to figure out the response wraps in {"chains": [...]} (not a bare array), then navigate 26 fields per chain to find solverAddresses — which is an array, not a string.
With the CLI — one command:
relay chains list --fields "chains[].{id: id, name: name, solvers: solverAddresses}"Output is 12KB (96% smaller), just what you need:
[
{
"id": 1,
"name": "ethereum",
"solvers": [
"0xf70da97812cb96acdf810712aa562db8dfa3dbef",
"0x56c262027e0de4aea31d2489529cb25d23e58a8b",
"0xabb2acd3be814a80e502575d6c1dc5f789e9cd10",
"0xa67d7eb4dc68fa6ce8e34ef8cadaf075b9893fbb",
"0xada5bb90d0de0bd1b6f3938708f49295a8d1f7cb"
]
},
{
"id": 10,
"name": "optimism",
"solvers": ["0xf70da97812cb96acdf810712aa562db8dfa3dbef"]
}
]No guessing at response shape, no navigating 26 fields, no context window blowup. Agents can also run relay schema chains to inspect the response shape before making the call.
Another example — request tracking:
Raw GET /requests/v2?id=0x6701... returns 4.7KB with 3 levels of nesting (data.metadata.currencyIn.currency.symbol), 20+ keys in data alone (fees, slippage, inTxs, outTxs, failReason, refundFailReason...).
With the CLI:
relay requests list --id 0x6701... --preset request-summary{
"status": "failure",
"user": "0xbf7e33e01a0e390dc554090afb06d546849ed59c",
"recipient": "Ck6gNTVAZR69UrPEPc6vA3peAkLSxckadJXpLRqb3dny",
"from": "G",
"fromChain": 1625,
"fromAmount": "14.657701729840066706",
"fromUsd": "0.192045",
"to": "SOL",
"toChain": 792703809,
"toAmount": "0.000171691",
"toUsd": "0.035870",
"feesUsd": { "gas": "0.030467", "fixed": "0.004858", "price": "0.000088", "gateway": "0.002183" },
"referrer": "jmp",
"createdAt": "2025-08-14T04:52:18.674Z"
}4.7KB → 521 bytes. Status, amounts, fees, user, recipient — everything you need to triage a support ticket in one call.
No API key required for public endpoints — start querying immediately.
# Install
npm install
# List all supported chains
npx tsx src/cli.ts chains list
# Check chain health
npx tsx src/cli.ts chains health
# Search for tokens on Base
npx tsx src/cli.ts currencies search --params '{"chainId": 8453, "term": "USDC"}'
# Get a quote for bridging USDC from Base to Ethereum
npx tsx src/cli.ts quote --params '{
"user": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"originChainId": 8453,
"destinationChainId": 1,
"originCurrency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"destinationCurrency": "0x0000000000000000000000000000000000000000",
"amount": "1000000"
}'
# Track a request
npx tsx src/cli.ts requests list --id 0x1234...
# Human-friendly table output
npx tsx src/cli.ts chains list --output table --fields "chains[0:10].{id: id, name: name, displayName: displayName}"Output:
id name displayName
──────────────────────────
1 ethereum Ethereum
10 optimism Optimism
25 cronos Cronos
56 bsc BNB
100 gnosis Gnosis
With an API key (optional — enables rate limit increases):
# Set API key via environment variable
export RELAY_API_KEY=your-key
# Or persist in config
npx tsx src/cli.ts config set apiKey your-key
# Use testnet
npx tsx src/cli.ts --testnet chains listShortcuts for common operations:
relay status 0x123... # → GET /requests/v2?id=... (quick status lookup)
relay tx 0x123... # → prints relay.link/transaction/... URL
# Human-friendly bridge quoting with chain names instead of IDs
relay bridge --from base --to eth --token USDC --amount 100 --user 0x...The CLI is designed agent-first: structured JSON output by default, consistent error envelopes, schema introspection, and context window protection via field filtering.
# Agent pattern: JSON output, machine-parseable
node relay-cli/dist/cli.js <command> [flags] --output json 2>/dev/null
# Two input modes
--params '{"raw": "json"}' # Maps directly to API body (preferred for agents)
--flag value # Individual flags (preferred for humans)Exit codes: 0 = success, 1 = error (validation, HTTP, or runtime).
| File | Purpose |
|---|---|
llms.txt |
Discovery entry point — links to all agent resources |
AGENTS.md |
Integration guide — invocation contract, safety tiers, gotchas |
CLAUDE.md |
Claude Code specific instructions and workflows |
agents/tool-catalog.json |
All commands with parameter schemas, types, safety flags |
agents/error-catalog.json |
7 error categories with retry strategies |
docs/relay-api-response-structures.md |
Field-by-field response docs with cross-status comparisons |
Agents can query response shapes at runtime instead of guessing:
relay schema chains # Response shape for GET /chains
relay schema quote.v2 # Request body schema for POST /quote/v2
relay schema --list # All public endpoints with methodsWhy Agent-First?
Most CLIs are built for humans and later adapted for automation. This creates friction for AI agents:
- Context windows — A 286KB chains response eats 25%+ of an agent's context. JMESPath filtering and built-in presets let agents request only the fields they need.
- Response shape discovery — Agents hallucinate field names. Schema introspection (
relay schema <endpoint>) gives them the actual shape before making calls. - Read-only by design — No execution endpoints. The CLI is for quoting, tracking, and discovery.
--dry-runprints the curl equivalent for review. - Consistent error format — Validation errors, HTTP errors, and runtime errors all follow the same structure. The error catalog documents retry strategies for each category.
- Auto-format detection — TTY gets tables, pipes get JSON. No
--output jsonflag needed in scripts.
21 public commands across 6 groups, auto-generated from the OpenAPI spec. New endpoints appear automatically without code changes.
| Group | Commands | Description |
|---|---|---|
| Chains | 3 | Chain discovery, health checks, liquidity |
| Quoting | 2 | Price estimates and full quotes with steps |
| Requests | 4 | Request tracking, metadata, signatures, status |
| Currencies | 5 | Token search, prices, trending, charts |
| App Fees | 2 | Fee balances, claim history |
| Utility | 5 | Schema, config, cache, status alias, tx link, bridge alias |
Full command reference
| Command | Method | Description |
|---|---|---|
relay chains list |
GET | All 80+ supported chains with solver addresses, contracts, config |
relay chains health |
GET | Health check for a chain by ID (disabled, lagging, solver balance) |
relay chains liquidity |
GET | Liquidity data across chains |
| Command | Method | Description |
|---|---|---|
relay quote |
POST | Full quote for a cross-chain bridge or swap — includes steps, fees, time estimate |
relay price |
POST | Lightweight price estimate (no steps/txs, faster than quote) |
| Command | Method | Description |
|---|---|---|
relay requests list |
GET | Get request details by ID, tx hash, or user address |
relay requests metadata |
POST | Submit metadata for a request |
relay requests signature |
GET | Get the signature for a request |
relay intents status |
GET | Lightweight status check (faster than /requests) |
| Command | Method | Description |
|---|---|---|
relay currencies search |
POST | Search for tokens across chains |
relay currencies token-price |
GET | Get token price by chain and address |
relay currencies trending |
GET | Trending tokens |
relay currencies chart |
GET | Price chart for a token |
relay prices rates |
GET | Current USD rates for tokens |
| Command | Method | Description |
|---|---|---|
relay app-fees balances |
GET | Claimable app fee balances for a wallet |
relay app-fees claims |
GET | List past app fee claims |
| Command | Method | Description |
|---|---|---|
relay config get |
GET | Relay protocol configuration |
relay swap-sources |
GET | Available swap sources (DEX aggregators, AMMs) |
relay transactions index |
POST | Tell Relay to index a transaction |
relay transactions single |
POST | Get a single transaction by chain and hash |
Fuzzy matching with aliases — use names instead of chain IDs:
relay bridge --from base --to eth --token USDC --amount 100 --user 0x...
# "base" → 8453, "eth" → 1Built-in aliases:
| Alias | Chain | ID |
|---|---|---|
eth |
Ethereum | 1 |
base |
Base | 8453 |
arb |
Arbitrum | 42161 |
op |
Optimism | 10 |
poly, matic |
Polygon | 137 |
bnb |
BNB Smart Chain | 56 |
avax |
Avalanche | 43114 |
sol |
Solana | 792703809 |
btc |
Bitcoin | 8253038 |
Unknown input gets Dice-coefficient similarity suggestions:
Unknown chain "bae". Did you mean:
8453 (Base)
56 (BNB Smart Chain)
1 (Ethereum)
USDC, USDT, and ETH are resolved to correct contract addresses per chain (Ethereum, Base, Optimism, Polygon, Arbitrum, Avalanche). The CLI handles the mapping so agents don't need to look up contract addresses.
Three output formats, auto-detected based on environment.
relay chains list --output json --fields "chains[0:3].{id: id, name: name}"[
{ "id": 1, "name": "ethereum" },
{ "id": 10, "name": "optimism" },
{ "id": 25, "name": "cronos" }
]relay chains list --output table --fields "chains[0:5].{id: id, name: name, displayName: displayName}"id name displayName
──────────────────────────
1 ethereum Ethereum
10 optimism Optimism
25 cronos Cronos
56 bsc BNB
100 gnosis Gnosis
relay chains list --output minimal --fields "chains[0:5].{id: id, name: name}"1 ethereum
10 optimism
25 cronos
56 bsc
100 gnosis
Auto-detection: TTY → table, pipe → JSON. Override with --output <format>.
Validation errors are caught before HTTP requests:
$ relay requests list --id invalid
Validation errors:
id: Request ID must start with 0x (got "invalid")
Preview any POST request without executing it:
$ relay quote --params '{"user":"0xd8dA...","originChainId":8453,...}' --dry-runcurl -X POST 'https://api.relay.link/quote/v2' \
-H 'Content-Type: application/json' \
-d '{"user":"0xd8dA...","originChainId":8453,...}'
JMESPath expressions to shrink responses and protect context windows.
# 286KB chains response → ~2KB
relay chains list --fields "chains[].{id: id, name: name, display: displayName}"
# Just the request status
relay requests list --id 0x123... --fields "requests[0].status"
# Quote summary — fees, amounts, rate, time estimate
relay quote --params '{...}' --fields "details.{from: currencyIn.currency.symbol, to: currencyOut.currency.symbol, rate: rate, time: timeEstimate}"Common queries as named presets:
| Preset | Fields | Use Case |
|---|---|---|
chains-slim |
id, name, displayName, vmType, depositEnabled | Chain overview |
chains-health |
id, name, disabled, blockProductionLagging | Monitoring |
chains-ids |
id, name | Lookup table |
request-status |
id, status, createdAt, updatedAt | Quick status check |
request-summary |
status, user, amounts, fees, timestamps | Full request overview |
quote-summary |
operation, tokens, amounts, rate, slippage, time | Quote comparison |
quote-fees |
relayerGas, relayerService, app, subsidized | Fee breakdown |
relay chains list --preset chains-slim
relay chains list --preset chains-health
relay requests list --id 0x123... --preset request-summaryValidates before HTTP requests with helpful messages:
| Input | Validation | Example |
|---|---|---|
| Addresses | 0x-prefixed, 40 hex chars | 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 |
| Request IDs | 0x-prefixed, 64 hex chars | 0x1234567890abcdef... |
| Chain IDs | Positive integers | 8453, 1, 42161 |
| Amounts | Numeric strings (wei, no decimals) | "1000000" |
| Tx hashes | 0x-prefixed, 64 hex chars | 0xabcdef... |
Wrong address format? The CLI suggests corrections:
Address must start with 0x. Did you mean: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045?
| Guard | What it does |
|---|---|
| Read-only | No execution endpoints — the CLI cannot submit transactions |
--dry-run |
Available on all POST endpoints — prints the curl equivalent without executing |
| TTY detection | Table output for humans, JSON for scripts. No accidental raw JSON walls in terminals |
| Input validation | Catches bad addresses, chain IDs, amounts before the HTTP request is made |
| Testnet flag | --testnet switches to api.testnets.relay.link — test without touching mainnet |
Three sources, in priority order:
- Flag:
--api-key your-key(highest priority) - Environment:
export RELAY_API_KEY=your-key - Config file:
~/.relay-cli/config.json(persisted, mode 0600)
Security note:
~/.relay-cli/config.jsonmay contain your API key. Do not commit it to version control or share it.
# Set API key in config
relay config set apiKey your-key
# View current config
relay config getThe OpenAPI spec is cached for 24 hours at ~/.relay-cli/cache/.
# Force refresh
relay --refresh-cache chains list
# Clear all cached data
relay cache clear# Use testnet API
relay --testnet chains list
# Testnet uses api.testnets.relay.link# Get health for all chains, filter to just status fields
relay chains health --fields "chains[].{id: id, name: name, disabled: disabled, lagging: blockProductionLagging}"relay quote \
--params '{"user":"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045","originChainId":8453,"destinationChainId":1,"originCurrency":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","destinationCurrency":"0x0000000000000000000000000000000000000000","amount":"1000000"}' \
--preset quote-summary# Quick status
relay status 0x1234...
# Full details with amounts and fees
relay requests list --id 0x1234... --preset request-summary
# Get the relay.link URL to share
relay tx 0x1234...# All chains with their solvers
relay chains list --fields "chains[].{id: id, name: name, solvers: solverAddresses}"
# Just one chain
relay chains list --fields "chains[?id==\`8453\`].{name: name, solvers: solverAddresses}"# Which chains have more than 1 solver?
relay chains list --output json | jq '[.chains[] | select(.solverAddresses | length > 1) | {id, name, count: (.solverAddresses | length)}]'
# All EVM chains
relay chains list --output json | jq '[.chains[] | select(.vmType == "evm") | .name]'src/
├── cli.ts # Entry point, commander setup, smart aliases
└── core/
├── spec-loader.ts # Fetch + cache OpenAPI spec (24h TTL)
├── command-builder.ts # OpenAPI paths → commander commands at runtime
├── executor.ts # HTTP execution, curl generation, error handling
├── formatter.ts # JSON / table / minimal output + JMESPath presets
├── validator.ts # Address/chainId/requestId/amount validation
├── chain-resolver.ts # Chain name→ID fuzzy matching + token resolution
├── cache.ts # Disk cache (~/.relay-cli/cache/) with TTL
└── auth.ts # API key: env → config → flag priority
- On startup, fetches the OpenAPI spec from
api.relay.link/documentation/json(cached 24h) - Filters out admin/internal and execution endpoints
- Deduplicates versioned paths (
/quote+/quote/v2→ keeps v2 only) - Generates commander subcommands with appropriate flags for each endpoint
- On command execution: validates inputs → resolves chains/tokens → makes HTTP request → formats output
| Package | Purpose |
|---|---|
commander |
CLI framework with programmatic command building |
jmespath |
JMESPath field filtering for response data |
Zero runtime dependencies beyond these two. The CLI is ~800 lines of TypeScript.