---
title: "freaky.fi Plugin"
description: "Skill plugin reference for fetching ranked DEX-aggregator quotes from freaky.fi and executing the resulting regular-flow swap via Base MCP send_calls. v1 covers GET-only reads against swap.freaky.fi and regular-flow adapters that return ready-to-broadcast tx + ERC-20 approval."
---

# freaky.fi Plugin

> [!IMPORTANT]
> Complete the short Base MCP onboarding flow defined in `SKILL.md` before calling any freaky.fi endpoint. The user's wallet address - used as `userAddress` on every `/rank` query - is fetched via `get_wallets` when needed.

freaky.fi is a DEX aggregator that ranks swap quotes across multiple vendors (1inch, KyberSwap, LI.FI, Velora/ParaSwap, OKX DEX, Bungee, OpenOcean, and others) and returns a ready-to-broadcast transaction plus a standard ERC-20 approval per quote. The plugin returns **unsigned** call data; signing and broadcasting are the wallet's job (Base MCP `send_calls`).

## Surface routing

| Capability | Hosts | Where it runs |
| --- | --- | --- |
| **Quote ranking (GET-only)** - fan-out to every adapter, ranked by best execution | `swap.freaky.fi` | Every surface. Use the harness HTTP tool when available; otherwise Base MCP `web_request` - this host returns `Access-Control-Allow-Origin: *` and is fine for server-side fetch. |
| **POST endpoints** (`/swap/prepare`, `/swap/relay`, `/swap/assemble`) - off-chain signing flows, Permit2, CowSwap, 0x Gasless | `swap.freaky.fi` | CLI harnesses only. **Not on the Base MCP `web_request` allowlist yet**, so v1 of this plugin sticks to GET-only `/rank`. See [Forward references](#forward-references). |
| **Chat-only fallback** - cases the regular-flow plan cannot cover | `freaky.fi` (web UI) | Chat-only surfaces, when the wallet is not a Base Account or the user wants typed-data flows. See [Chat-only fallback](#chat-only-fallback-freaky-fi-ui). |

Routing order for any freaky.fi HTTP call:

1. **Harness HTTP tool** (`curl`, `fetch`, shell) - works for every host, any method, no allowlist.
2. **Base MCP `web_request`** - chat-only surfaces, GET-only against `swap.freaky.fi`.
3. **freaky.fi web UI** - chat-only surfaces, anything that needs POST or off-chain signing. See [Chat-only fallback](#chat-only-fallback-freaky-fi-ui).

No API key or Authorization header is required for `/rank`.

Do not sign, approve, or submit transactions unless the user explicitly asks. Generating call data and `send_calls` approval links is safe; the user approves any real transaction.

---

## API Services

| Service | Base URL | Routing | Purpose |
| --- | --- | --- | --- |
| swap | `https://swap.freaky.fi` | Every surface (GET) | Quote ranking across DEX aggregators. Returns per-quote `tx` and `approval` envelopes that map directly to a `send_calls` batch. |

Source of truth for the live request and response shape:

```
GET https://swap.freaky.fi/doc    # OpenAPI 3.0 JSON
GET https://swap.freaky.fi/docs   # Scalar UI
```

If a field in this document drifts from the live OpenAPI doc, the OpenAPI doc wins. Fetch it once at the start of a session if you need authoritative names.

---

## Chain Support And Rules

v1 of this plugin is focused on Base mainnet (`chainId` 8453) because that is where Base MCP `send_calls` runs natively via the Base Account smart wallet. `/rank` itself works on most EVM chains. The authoritative supported chain list is at [`list.freaky.fi/chains/evm?onlyPopular=false`](https://list.freaky.fi/chains/evm?onlyPopular=false) - hit that endpoint when the user asks which chains they can swap on, or when validating a non-Base `chainId` before fetching a quote. For chains where the user does not have a Base Account, fall back to the [chat-only path](#chat-only-fallback-freaky-fi-ui).

Canonical token addresses worth pinning for the default Base flow:

| Symbol | Chain | Address |
| --- | --- | --- |
| USDC | Base 8453 | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| WETH | Base 8453 | `0x4200000000000000000000000000000000000006` |
| ETH (native) | any | `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` (pass this as `from` or `to` for native ETH; case-insensitive) |

The plugin does not ship a symbol-to-address resolver. When the user names a token by symbol, resolve it yourself (e.g. via the wallet's token list, a chain explorer, or a known mapping) before calling `/rank`.

---

## Adapter Filter For v1

`/rank` queries every enabled adapter by default. v1 of this plugin restricts to **regular-flow adapters only** by passing the existing `?aggregators=` filter. Regular-flow means: the adapter returns a ready-to-broadcast `tx` plus a standard ERC-20 `approve` (or no approval, for native ETH input). No Permit2, no off-chain signing, no separate relay step.

| Input | `aggregators` value |
| --- | --- |
| ERC-20 `from` | `velora,kyberswap,lifi,okx-dex,bungee,1inch,openocean` |
| Native ETH `from` (`0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`) | `velora,kyberswap,lifi,okx-dex,bungee,1inch,openocean,odos` |

Why this filter is required for v1:

- **CowSwap** and **0x Gasless** are off-chain signing flows. They require EIP-712 typed-data signing plus a follow-up `POST /swap/relay` to hand the signed order to the upstream venue. That POST is not on the Base MCP `web_request` allowlist yet (tracked in [#203](https://github.com/freaky-fi/freaky.fi/issues/203)) and is not part of this read-only plugin's surface.
- **OpenOcean Permit2**, **0x v2**, and **Odos** for ERC-20 inputs use **Permit2**. Base MCP exposes `sign-permit` so the EIP-712 leg is solvable, but the flow still needs a follow-up `POST /swap/assemble` to fold the signature into the final tx. Same allowlist constraint as above.
- The Base Account smart wallet already batches `[approve, swap]` atomically in one user approval, so Permit2's gas-savings advantage does not apply on this surface.
- Odos is included only for native-ETH input because native input has no approval step and therefore no Permit2 leg.

When the user is on a chain without a Base Account, or wants an adapter outside the regular-flow set, the [chat-only fallback](#chat-only-fallback-freaky-fi-ui) covers it: the freaky.fi widget handles Permit2 / off-chain-signing flows natively.

When [issue #194](https://github.com/freaky-fi/freaky.fi/issues/194) lands, `/rank` will accept `?prefer=regular` and you can drop the hardcoded list. Until then, send the list verbatim.

---

## Request Shape

```
GET https://swap.freaky.fi/rank
  ?chainId=<number>
  &from=<sell-token-address>
  &to=<buy-token-address>
  &amount=<string, wei>
  &userAddress=<0x-address>
  &slippage=<percent, e.g. 0.5>
  &aggregators=velora,kyberswap,lifi,okx-dex,bungee,1inch,openocean
  [&side=sell|buy]
  [&timeout=<ms, 1000-15000>]
  [&fromDecimals=<int>]
  [&toDecimals=<int>]
```

| Param | Required | Notes |
| --- | --- | --- |
| `chainId` | yes | EVM chain ID. Base = `8453`. |
| `from` | yes | Sell token address. Use `0xeee...eee` for native. |
| `to` | yes | Buy token address. Use `0xeee...eee` for native. |
| `amount` | yes | Amount in smallest unit (wei for ETH, `1e6` for USDC). String, no decimals. |
| `userAddress` | yes | From `get_wallets`. Adapters use it to scope quotes and compute the correct `approval.spender`. |
| `slippage` | yes | Percent, not bps. `0.5` means 0.5%. Values above `5` return `success: false` with a message. |
| `side` | no | `sell` (default) = exact input, `buy` = exact output. Buy-side pre-filters to adapters that natively support `exactOutput`. |
| `timeout` | no | Per-adapter timeout in ms. Clamped to `[1000, 15000]`. Default `5000`. |
| `fromDecimals`, `toDecimals` | no | Optional hints. Some adapters skip an on-chain decimals lookup when provided. |
| `aggregators` | no | Comma-separated adapter IDs to whitelist. **Pass the v1 list above.** |

---

## Response Shape

```json
{
  "success": true,
  "quotes": [
    {
      "rank": 1,
      "id": "velora-0x...-1700000000000",
      "name": "Velora",
      "amountIn":  "1000000",
      "amountOut": "401234567890123456",
      "tx": {
        "to":       "0x...",
        "data":     "0x...",
        "value":    "0x0",
        "gasLimit": "350000"
      },
      "approval": {
        "token":   "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "spender": "0x...",
        "amount":  "1000000"
      },
      "metadata": {
        "adapter":  "velora",
        "provider": "paraswap",
        "name":     "Velora"
      },
      "timeToQuote": 312,
      "_sig": "<base64 ed25519 signature, optional>"
    }
  ],
  "skipped": [{ "adapter": "1inch", "reason": "Adapter returned no quote" }],
  "errors":  [{ "adapter": "openocean", "error": "Timeout after 5000ms", "type": "timeout" }],
  "widgetUrl": "https://freaky.fi/?chain=8453&sell=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&buy=0x4200000000000000000000000000000000000006&amount=1&slippage=0.5",
  "metadata": {
    "totalAdapters": 7,
    "successful":    4,
    "skipped":       1,
    "timedOut":      1,
    "failed":        1,
    "responseTime":  "612ms"
  }
}
```

| Field | Meaning |
| --- | --- |
| `success` | `true` if the request was well-formed. `false` for validation errors (e.g. slippage above 5%). |
| `quotes[]` | Sorted best to worst. `rank: 1` is the best execution: highest `amountOut` for sell-side, lowest `amountIn` for buy-side. |
| `quotes[].tx` | Ready-to-broadcast call. Forward `to`, `data`, `value` into `send_calls` verbatim. `gasLimit` is the adapter's hint; the wallet may override it. |
| `quotes[].approval` | `null` when the input is native ETH. Non-null when an ERC-20 `approve(spender, amount)` is required first. Adapters do not check existing on-chain allowance, so the field is populated for every ERC-20 input. |
| `quotes[].metadata.adapter` | The adapter ID. Useful for logging or for telling the user which venue executed the swap. |
| `quotes[]._sig` | Optional Ed25519 signature over the quote payload. Informational for the v1 plugin flow; treat as opaque. |
| `skipped[]` | Adapters that returned no quote (no route, pair unsupported). Not an error. |
| `errors[]` | Adapters that timed out or threw. `type` is `timeout` or `error`. |
| `widgetUrl` | One-click deeplink to the freaky.fi widget, pre-filled with the swap intent. Use it as the universal execution path when `send_calls` is not available, when the user prefers their own wallet, or when no route was returned. `amount` is populated only when the request included `fromDecimals`. |
| `metadata.responseTime` | End-to-end fan-out time. Useful for diagnostics. |

If `quotes` is empty, every adapter either skipped or errored. See [Error handling](#error-handling).

---

## Units

| Surface | Unit behavior |
| --- | --- |
| `/rank` query `amount` | Wei. Smallest unit of the `from` token. String, no decimals. |
| `/rank` response `amountIn`, `amountOut` | Wei. Smallest unit. String. |
| `/rank` response `approval.amount` | Wei. Smallest unit of `approval.token`. |
| `/rank` response `tx.value` | Hex-prefixed wei (`0x`-style) for the native-currency value of the call. `"0x0"` for ERC-20-in swaps. |
| `/rank` response `tx.gasLimit` | Decimal gas units, stringified. Use as a hint only; the wallet will re-estimate. |

Resolve token decimals yourself (token list, on-chain `decimals()`, or a known table) before converting human inputs to `amount` and before showing the user the human-readable output. The plugin does not return decimals.

---

## Orchestration Pattern (regular-flow swap)

The full happy path, GET-only, every step idempotent:

```
get_wallets                                                    -> userAddress
GET swap.freaky.fi/rank?chainId=...&from=...&to=...            -> ranked quotes
                       &amount=...&userAddress=...&slippage=...
                       &aggregators=<v1 set>
pick quotes[0]                                                 -> best execution
build [approveCall?, swapCall] from quote.approval and quote.tx
send_calls { chain: "base", calls: [...] }                     -> approval URL + requestId
surface approval URL to the user
get_request_status(requestId)                                  -> poll until non-pending
```

Step-by-step:

1. **Get the user's address.** Call `get_wallets` from Base MCP. Use the Base Account address as `userAddress` on the next call.
2. **Fetch ranked quotes.**

   ```
   GET https://swap.freaky.fi/rank
     ?chainId=8453
     &from=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
     &to=0x4200000000000000000000000000000000000006
     &amount=1000000
     &userAddress=<0x-address>
     &slippage=0.5
     &aggregators=velora,kyberswap,lifi,okx-dex,bungee,1inch,openocean
   ```

   For a USDC->WETH swap of 1 USDC on Base, the example above is complete.
3. **Pick the top-ranked quote.** `quotes[0]` is the best execution. Higher-ranked alternatives exist only as fallback if the top one fails to broadcast; for v1, just take the first.
4. **Build the call array.**

   - If `quote.approval` is non-null, prepend an ERC-20 `approve` call. The function selector is `0x095ea7b3` and the calldata is the selector followed by the spender padded to 32 bytes and the amount padded to 32 bytes.

     ```
     approveCall = {
       to:    quote.approval.token,
       data:  "0x095ea7b3"
              + quote.approval.spender.slice(2).toLowerCase().padStart(64, "0")
              + BigInt(quote.approval.amount).toString(16).padStart(64, "0"),
       value: "0x0"
     }
     ```

   - The swap call is the `tx` envelope verbatim.

     ```
     swapCall = { to: quote.tx.to, data: quote.tx.data, value: quote.tx.value }
     ```

   - For native-ETH input, `approval` is `null`; the calls array is just `[swapCall]`.

5. **Send the batch.**

   ```
   send_calls({
     chain: "base",
     calls: [approveCall, swapCall]   // or [swapCall] for native input
   })
   ```

   Map other chains by name (`"optimism"`, `"arbitrum"`, `"polygon"`, ...). For non-Base chains the user must have a Base Account that supports them, or fall back to the [web UI](#chat-only-fallback-freaky-fi-ui).
6. **Surface the approval URL.** `send_calls` returns a URL where the user signs the bundle. Show it as a link.
7. **Poll for execution.** Call `get_request_status(requestId)` until it returns non-pending. Report the final tx hashes back to the user.

---

## Batching With send_calls

All calls in one `send_calls` batch share a single user approval and execute atomically inside the Base Account smart wallet. This is precisely why v1 sticks to regular-flow adapters: the smart wallet already provides atomic batching natively, so Permit2's gas-savings advantage (combining `permit` and `swap` in one tx) does not apply on this surface. Add a third call to the batch (e.g. a follow-up `transfer`) freely - the user still signs once.

For native-ETH input, the batch is a single call; `send_calls` still works.

---

## Error handling

| Condition | What to do |
| --- | --- |
| `/rank` returns `success: false` with a `message` | Surface the message verbatim. Typical causes: `slippage` above 5%, malformed address. |
| `quotes` is empty | Tell the user no route was available. Suggest a different pair, a larger amount (small notionals are often skipped), or a different chain. List the `skipped` and `errors` adapters if asked. |
| Selected quote has `approval` but the user already approved the spender for at least `amount` | Optional optimization: skip `approveCall` after an on-chain `allowance(owner, spender)` check. If unsure, just include it - re-approving an already-approved spender is a no-op except for gas. |
| `send_calls` is rejected by the user | Stop. Do not retry. Acknowledge and offer to re-quote with different parameters. |
| `send_calls` returns an error | Surface the error verbatim. Common causes: insufficient balance, insufficient gas, quote expired (re-quote and retry). |
| `get_request_status` returns a revert | Show the revert reason. Common causes: slippage exceeded between quote and execution (re-quote with a slightly higher `slippage` and retry), allowance race (re-quote, ensure `approveCall` is included). |

Never silently retry a `/rank` call with adjusted parameters; ask the user.

---

## Forward references

| Issue | Effect on this plugin |
| --- | --- |
| [#193](https://github.com/freaky-fi/freaky.fi/issues/193) | Adds `recommendedGasLimit` per quote to `/rank`. When it lands, prefer `quote.recommendedGasLimit` over the raw `quote.tx.gasLimit` when synthesizing `send_calls`. |
| [#194](https://github.com/freaky-fi/freaky.fi/issues/194) | Adds a `?prefer=regular` shortcut to `/rank`. When it lands, drop the hardcoded `aggregators=velora,...` list and pass `?prefer=regular` instead. New regular-flow adapters added later get picked up automatically. |
| Base team allowlists `swap.freaky.fi` for POST in `web_request` | Unlocks `/swap/prepare`, `/swap/relay`, `/swap/assemble`. With those, this plugin can extend to Permit2 (Odos, OpenOcean, 0x v2), CowSwap, and 0x Gasless. Until then, v1 stays GET-only. |

None of these block v1. The plugin works as documented today against the regular-flow adapter set.

---

## Chat-only fallback: freaky.fi UI

When the regular-flow plan does not fit - the user is not on a Base Account, the user wants to use a Permit2 or CowSwap or 0x Gasless route, the user is on a chain where Base MCP `send_calls` is not available, or the v1 adapter set returns no route - surface `response.widgetUrl` from the `/rank` response directly to the user. It is a one-click link to the freaky.fi widget pre-filled with their swap intent; they complete the swap in any wallet. The widget handles Permit2 and off-chain-signing flows natively, so this path covers every adapter, not just the regular-flow set.

```
https://freaky.fi/?chain=...&sell=...&buy=...&amount=...&slippage=...
```

The URL schema is already populated for you. Do not reconstruct it by hand - take `widgetUrl` from the `/rank` response as-is. Do not claim the swap was executed; the UI flow is user-driven.

Only use this fallback when the GET-only `/rank` plus `send_calls` plan cannot satisfy the request. The regular-flow path covers the common case (USDC <-> WETH, USDC <-> ETH, stablecoin <-> stablecoin on Base) end to end.
