For agents

Two surfaces, one pipeline

An agent can talk to Routey two ways: through the MCP server, which is ergonomic for LLM-driven workflows, or through the REST API, which is ergonomic for everything else. Both call the same library code that the web UI calls.

SurfaceTransportUse when
MCP serverstdio, JSON-RPCAn LLM agent (Claude Code, Claude Desktop, any MCP client) needs to call swap tools with typed inputs and structured outputs.
REST APIHTTP/JSON + SSEA bot, a trading script, a dashboard, or any non-MCP runtime needs to fetch quotes, execute swaps, or consume a live event stream.

MCP server

Installation

The server is a single TypeScript file at mcp/index.ts. Run it with npx tsx over stdio. Any MCP client can host it.

Claude Code

terminal
claude mcp add routey -- npx tsx /absolute/path/to/routey/mcp/index.ts

Verify the server is reachable:

terminal
claude mcp list

Claude Desktop

Add an entry to your claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/ — Windows: %APPDATA%/Claude/):

claude_desktop_config.json
{
  "mcpServers": {
    "routey": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/routey/mcp/index.ts"]
    }
  }
}

Any MCP client

Launch the server with stdio transport:

terminal
npx tsx /path/to/routey/mcp/index.ts

The server announces itself as routey v1.0.0 and exposes four tools (listed below).

Signing key

get_quote, get_supported_chains, and get_supported_tokens are read-only and require no key.

execute_swap needs a private key to sign the transaction. Two ways to supply it, in priority order:

  1. Pass it explicitly as the private_key tool argument. Used for the exact call and discarded.
  2. Set AGENT_PRIVATE_KEY in the process environment when launching the server. Used as a fallback when the tool call does not provide one.
Treat the signing key like you would treat it anywhere else
Routey does not persist the key, log the key, or transmit it past the viem wallet client that signs the single transaction. That does not make it safe to hand an untrusted agent a hot-wallet key to your main account — create a dedicated funding wallet for agent use, load it with the budget you are willing to lose, and rotate if a session ends badly.

Tool reference

get_quote

Fetches quotes from both providers and returns both plus the winner. Read-only, no signing. Typically returns in 300–500 ms depending on RPC latency.

Input

FieldTypeRequiredNotes
tokenInstringyesSymbol, e.g. "ETH", "USDC".
tokenOutstringyesSymbol.
amountstringyesHuman units (e.g. "0.5"). Decimals are applied server-side.
chainstringnoDefaults to "ethereum". One of ethereum | base | arbitrum | optimism.

Example call

agent prompt
Use routey:get_quote to quote 0.5 ETH → USDC on ethereum.

Example response

tool result
{
  "uniswap": {
    "provider":   "uniswap",
    "amountOut":  "1234.56",
    "fee":        "0.05%",
    "gasEstimate":"$3.20",
    "latencyMs":  180
  },
  "relay": {
    "provider":   "relay",
    "amountOut":  "1231.70",
    "fee":        "0%",
    "gasEstimate":"$0.00",
    "latencyMs":  220
  },
  "best":        "uniswap",
  "savedAmount": "2.86"
}

execute_swap

Executes a swap through the chosen (or best) provider and returns the transaction hash once broadcast. Signs with the private key as described in MCP server → Signing key.

Input

FieldTypeRequiredNotes
tokenInstringyesSymbol.
tokenOutstringyesSymbol.
amountstringyesHuman units.
chainstringnoDefault "ethereum".
slippagenumbernoPercent. Default 0.5.
providerenumno"uniswap" | "relay" | "best". Default "best" (re-quotes and picks the winner at execution time).
private_keystringno0x… hex. Falls back to AGENT_PRIVATE_KEY env var.

Example response

tool result
{
  "txHash":    "0x3a7f9b2c8e1d5a4b6c9e2f8d7a3b5e9c1f4a7b2c9e8d5f3a1b7c4e9d2a8b6f5e",
  "provider":  "uniswap",
  "amountOut": "1234.56"
}

get_supported_chains

No arguments. Returns the four chains with their IDs, names, and slugs, so an agent can enumerate routes before asking a user which network to execute on.

Example response

tool result
[
  { "id": 1,     "name": "Ethereum", "slug": "ethereum" },
  { "id": 8453,  "name": "Base",     "slug": "base"     },
  { "id": 42161, "name": "Arbitrum", "slug": "arbitrum" },
  { "id": 10,    "name": "Optimism", "slug": "optimism" }
]

get_supported_tokens

Takes a chain slug (defaults to ethereum) and returns the tokens available on that chain.

Input

FieldTypeRequiredNotes
chainstringnoDefault "ethereum".

Example response

tool result
[
  { "symbol": "ETH",  "name": "Ethereum",    "address": "0x0000000000000000000000000000000000000000" },
  { "symbol": "USDC", "name": "USD Coin",    "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" },
  { "symbol": "USDT", "name": "Tether",      "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7" },
  { "symbol": "WBTC", "name": "Wrapped BTC", "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" }
]

REST API

Base URL is the origin the Next.js app is served from — in development that is http://localhost:3000. There is no authentication; if you want to lock it down, put the server behind your own reverse proxy or add middleware.

GET /api/quote

Fetches both provider quotes and returns the winner.

Query parameters

NameTypeDefaultNotes
tokenInstring"ETH"Symbol.
tokenOutstring"USDC"Symbol.
amountstring"0"Must parse to a positive decimal.
chainstring"ethereum"Chain slug.

Example

curl
curl -sS "http://localhost:3000/api/quote?tokenIn=ETH&tokenOut=USDC&amount=0.5&chain=ethereum"

Response

A QuoteResult — see Types.

200 OK
{
  "uniswap":     { "provider": "uniswap", "amountOut": "1234.56", "fee": "0.05%", "gasEstimate": "$3.20", "latencyMs": 180 },
  "relay":       { "provider": "relay",   "amountOut": "1231.70", "fee": "0%",    "gasEstimate": "$0.00", "latencyMs": 220 },
  "best":        "uniswap",
  "savedAmount": "2.86"
}

Errors

  • 400 UNKNOWN_CHAIN — unknown chain slug.
  • 400 INVALID_AMOUNT — amount missing or not > 0.

POST /api/swap

Executes a swap. Signs using either the privateKey field in the body or the AGENT_PRIVATE_KEY environment variable.

Request body

SwapRequest
{
  "tokenIn":    "ETH",
  "tokenOut":   "USDC",
  "amount":     "0.5",
  "chain":      "ethereum",
  "slippage":   0.5,
  "provider":   "best",
  "privateKey": "0x…"
}

slippage, provider, and privateKey are optional. The other four are required.

Example

curl
curl -sS -X POST http://localhost:3000/api/swap \
  -H 'content-type: application/json' \
  -d '{
    "tokenIn":  "ETH",
    "tokenOut": "USDC",
    "amount":   "0.5",
    "chain":    "ethereum",
    "slippage": 0.5,
    "provider": "best"
  }'

Response

200 OK
{
  "txHash":    "0x3a7f9b2c…b6f5e",
  "provider":  "uniswap",
  "amountOut": "1234.56"
}

Errors

  • 400 INVALID_JSON — body failed to parse.
  • 400 MISSING_FIELDS — one of the four required fields is missing.
  • 400 UNKNOWN_CHAIN — unknown chain slug.
  • 500 SWAP_FAILED — executor error. The error field carries the underlying message.

GET /api/events (SSE)

Opens a server-sent event stream. Every swap lifecycle event, whether initiated from the web UI or the API, is multicast to every open connection.

Response is text/event-stream with data: {…json…} frames. A : heartbeat comment is sent every 25 seconds so idle connections survive intermediary timeouts.

Event stream reference

Event types

agent_connect

An MCP session or API client opened a new stream.

payload
{ "agentId": "agent_k9j4nz2sp7d3" }

quote_request

A quote was requested.

payload
{ "amount": "0.5", "tokenIn": "ETH", "tokenOut": "USDC" }

quote_result

A quote request returned. The payload is a full QuoteResult.

swap_execute

A swap was broadcast to the chain.

payload
{
  "tokenIn":  "ETH",
  "tokenOut": "USDC",
  "amount":   "0.5",
  "chain":    "ethereum",
  "slippage": 0.5,
  "provider": "uniswap"
}

swap_confirmed

The swap transaction mined.

payload
{
  "txHash":    "0x3a7f9b2c…b6f5e",
  "provider":  "uniswap",
  "amountOut": "1234.56"
}

Consuming the stream

browser / deno
const es = new EventSource('http://localhost:3000/api/events')

es.onmessage = (e) => {
  const event = JSON.parse(e.data)
  switch (event.type) {
    case 'agent_connect':  console.log('agent', event.payload.agentId); break
    case 'quote_request':  console.log('quote', event.payload); break
    case 'quote_result':   console.log('best:', event.payload.best); break
    case 'swap_execute':   console.log('broadcast', event.payload.provider); break
    case 'swap_confirmed': console.log('tx', event.payload.txHash); break
  }
}
curl (shell consumer)
curl -N http://localhost:3000/api/events

Types

The complete type surface is in lib/types.ts. Reproduced here verbatim so the reference is self-contained.

lib/types.ts
export type ChainId = 1 | 8453 | 42161 | 10

export interface Chain {
  id: ChainId
  name: string
  slug: string
  rpcUrl: string
  explorerUrl: string
  nativeCurrency: { symbol: string; decimals: number }
}

export interface Token {
  symbol: string
  name: string
  decimals: number
  address: string       // 0x000…000 for native ETH
  chainId: ChainId
  logoURI: string
}

export type Provider = 'uniswap' | 'relay'

export interface Quote {
  provider: Provider
  amountOut: string     // human-readable, e.g. "1234.00"
  fee: string           // e.g. "0.05%"
  gasEstimate: string   // e.g. "$3.20"
  latencyMs: number
}

export interface QuoteResult {
  uniswap: Quote | null
  relay:   Quote | null
  best:    Provider | null
  savedAmount: string   // e.g. "2.80"
}

export interface SwapRequest {
  tokenIn:  string
  tokenOut: string
  amount:   string
  chain:    string
  slippage?: number
  provider?: Provider | 'best'
  privateKey?: string
}

export interface SwapResult {
  txHash:    string
  provider:  Provider
  amountOut: string
}

export type SSEEvent =
  | { type: 'agent_connect';  payload: { agentId: string } }
  | { type: 'quote_request';  payload: { amount: string; tokenIn: string; tokenOut: string } }
  | { type: 'quote_result';   payload: QuoteResult }
  | { type: 'swap_execute';   payload: SwapRequest }
  | { type: 'swap_confirmed'; payload: SwapResult }

Error codes

CodeHTTPMeaning
UNKNOWN_CHAIN400The chain slug is not one of the four supported values.
INVALID_AMOUNT400Amount is missing, not a number, or ≤ 0.
INVALID_JSON400Request body failed to parse as JSON.
MISSING_FIELDS400One or more of the required fields (tokenIn, tokenOut, amount, chain) is missing.
SWAP_FAILED500The executor raised. Underlying message is in the error field.

End-to-end example

A self-contained TypeScript script: fetch a quote, check the savings, execute if the savings exceed a threshold, confirm from the event stream.

bot.ts
import type { QuoteResult, SwapResult, SSEEvent } from './lib/types'

const BASE = 'http://localhost:3000'

async function quote(tokenIn: string, tokenOut: string, amount: string, chain = 'ethereum') {
  const url = new URL(`${BASE}/api/quote`)
  url.searchParams.set('tokenIn',  tokenIn)
  url.searchParams.set('tokenOut', tokenOut)
  url.searchParams.set('amount',   amount)
  url.searchParams.set('chain',    chain)
  const res = await fetch(url)
  if (!res.ok) throw new Error(`quote failed: ${res.status}`)
  return res.json() as Promise<QuoteResult>
}

async function swap(tokenIn: string, tokenOut: string, amount: string, chain = 'ethereum') {
  const res = await fetch(`${BASE}/api/swap`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ tokenIn, tokenOut, amount, chain, slippage: 0.5, provider: 'best' }),
  })
  if (!res.ok) throw new Error(`swap failed: ${await res.text()}`)
  return res.json() as Promise<SwapResult>
}

function watch(onEvent: (e: SSEEvent) => void) {
  const es = new EventSource(`${BASE}/api/events`)
  es.onmessage = (m) => { try { onEvent(JSON.parse(m.data)) } catch {} }
  return () => es.close()
}

async function main() {
  const stop = watch((e) => {
    if (e.type === 'swap_confirmed') {
      console.log('✓', e.payload.txHash)
    }
  })

  const q = await quote('ETH', 'USDC', '0.5')
  if (!q.best) { console.log('no route'); stop(); return }

  const saved = parseFloat(q.savedAmount)
  if (saved < 1) { console.log('savings below threshold, skipping'); stop(); return }

  const tx = await swap('ETH', 'USDC', '0.5')
  console.log('broadcast', tx.txHash, 'via', tx.provider)
}

main().catch(console.error)
One pipeline, two signing surfaces
This script talks to the same /api/swap endpoint the web UI calls when a human clicks Swap Now. The executor only sees a signed transaction — whether the signer came from a browser wallet or a private key in AGENT_PRIVATE_KEY is below its level of abstraction.

Next