{
  "openapi": "3.1.0",
  "info": {
    "title": "Swap Router API",
    "version": "1",
    "description": "Prices swaps across curated pools on Ethereum, Base and Arbitrum, and returns the calldata for one.\n\nIt holds no keys, signs nothing and never broadcasts — the calldata it returns is signed and sent by the caller.\n\n## Amounts are strings, always\n\nEvery amount is a base-unit **decimal string**. Not a number. 1 WETH is `\"1000000000000000000\"`; 1 USDC is `\"1000000\"`.\n\nJSON numbers are IEEE-754 doubles and lose precision above 2^53 — which an 18-decimal amount passes at 0.01 tokens. A float here is a wrong amount, not a rounding artefact.\n\n## Three things that cost money if you get them wrong\n\n1. **`amountOutAfterFee` is what the user receives.** `amountOut` is gross, before the protocol fee. Showing `amountOut` to a user overstates their proceeds.\n2. **`minAmountOut` is yours to set, and it is the only protection the contract enforces.** `/v1/build` does not re-derive it. Passing one from a quote taken minutes ago is how a swap reverts after the user signed it.\n3. **`priceImpactBps` truncates.** A 12 bps impact reads as 0 under a 1 bp cap. Use `priceImpactPips` when the threshold is sub-bp.\n\n## Quotes are pinned to a block\n\nEvery response carries `blockNumber`. A quote is not valid indefinitely, and a build against a quote minutes old is a build against a price that has moved."
  },
  "servers": [{ "url": "https://swap.defiloops.com" }],
  "security": [{ "ApiKey": [] }],
  "tags": [
    { "name": "Quoting", "description": "What is this worth, and what do I send." },
    { "name": "Prices", "description": "Marginal rates and the live stream. Neither makes an RPC call." },
    { "name": "Discovery", "description": "What this instance serves." }
  ],
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Required on every `/v1/*` route. `/healthz`, `/readyz`, `/metrics` and this page do not need one."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": { "type": "string", "description": "Stable machine string. Switch on this.", "example": "no_route" },
          "message": { "type": "string", "description": "For humans. May change between releases." }
        }
      },
      "LimitRate": {
        "type": "object",
        "description": "A rate expressed as a **pair of base-unit amounts**, not a float — no decimals question and no buy/sell flag to invert. It is what the caller receives NET of every fee: both the LP fee and the protocol fee are undone before it reaches the pool.",
        "required": ["amountIn", "minAmountOut"],
        "properties": {
          "amountIn": { "type": "string", "example": "1000000000000000000" },
          "minAmountOut": { "type": "string", "example": "3400000000" }
        }
      },
      "Leg": {
        "type": "object",
        "description": "One hop of the route.",
        "properties": {
          "pool": { "type": "string" },
          "venue": { "type": "string", "example": "uniswap_v3" },
          "tokenIn": { "type": "string" },
          "tokenOut": { "type": "string" }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": ["chainId", "tokenIn", "tokenOut"],
        "additionalProperties": false,
        "description": "Exactly one of `amountIn` / `amountOut` must be present. Unknown fields are rejected — a typo'd key is a 400, not a silently ignored option.",
        "properties": {
          "chainId": {
            "type": "integer",
            "description": "**Required and not defaulted.** A default is wrong the moment more than one chain is served: omit it with Arbitrum addresses and you get `token_not_supported` for Base, which points at the whitelist instead of at the one field that was missing. `GET /v1/chains` lists what this instance serves.",
            "example": 8453
          },
          "tokenIn": { "type": "string", "example": "0x4200000000000000000000000000000000000006" },
          "tokenOut": { "type": "string", "example": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" },
          "amountIn": { "type": "string", "description": "Exact-input: \"I have this much, maximise my output.\"", "example": "1000000000000000000" },
          "amountOut": { "type": "string", "description": "Exact-output: \"I want exactly this much, minimise what it costs me.\"" },
          "slippageBps": { "type": "integer", "description": "Omit for the configured default, which is **10 bps (0.1%)**. Max 500. Presets a UI might offer: 10 / 25 / 50 / 100 — those are picker suggestions, not the default.", "example": 10 },
          "maxHops": { "type": "integer", "description": "Cap the hop count. Lower is faster and possibly worse-priced." },
          "allowSplit": { "type": "boolean", "description": "Allow splitting the order across pools." },
          "allowPartialFill": { "type": "boolean", "description": "Accept a short fill rather than a refusal." },
          "limitRate": { "$ref": "#/components/schemas/LimitRate" },
          "maxImpactBps": { "type": "integer", "description": "**Sizes the trade DOWN to fit the cap — it does not refuse it.** You get a smaller trade, not an error." },
          "costBps": { "type": "integer", "description": "Size against total cost rather than impact alone." }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "chainId": { "type": "integer" },
          "kind": { "type": "string", "example": "exact_in" },
          "action": { "type": "string", "example": "swap" },
          "tokenIn": { "type": "string" },
          "tokenOut": { "type": "string" },
          "amountIn": { "type": "string" },
          "amountOut": { "type": "string", "description": "GROSS, before the protocol fee. Not what the user receives." },
          "protocolFeeBps": { "type": "integer" },
          "protocolFeeAmount": { "type": "string" },
          "amountOutAfterFee": { "type": "string", "description": "**What the user actually receives.** Show this one." },
          "minAmountOut": { "type": "string", "description": "The floor, at the requested slippage. Pass this to /v1/build." },
          "maxAmountIn": { "type": "string" },
          "slippageBps": { "type": "integer" },
          "priceImpactBps": { "type": "integer", "description": "Truncates — 12 bps reads as 0 under a 1 bp cap." },
          "priceImpactPips": { "type": "integer", "description": "The same number at 100x resolution. Compare against this for sub-bp thresholds." },
          "lpFeeBps": { "type": "integer" },
          "gasEstimate": { "type": "string" },
          "blockNumber": { "type": "integer", "description": "The block this was priced at. A quote is not valid indefinitely." },
          "route": { "type": "array", "items": { "$ref": "#/components/schemas/Leg" } }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed. `identical_tokens`, `invalid_amount`, `native_wrap_not_a_swap`, `bad_request`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unsatisfiable": {
        "description": "**Well-formed and unsatisfiable — retrying is pointless.** `token_not_supported`, `unsupported_chain`, `no_route`, `token_not_simulatable`, `router_not_configured`, `pool_not_allowlisted`, `nothing_fills_at_limit`, `no_size_at_cost`, `cost_bound_unavailable`, `excessive_price_impact`.\n\nThe 400/422 split is load-bearing: these are understood requests that cannot be served, so the caller has to change the request or wait for a deploy. Alerting should not treat them as failures.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unavailable": {
        "description": "**Transient — retry.** `venues_unavailable`, `state_unavailable`, `rpc_unavailable`. The router's own state read failed or went stale. These are the ones worth paging on.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  },
  "paths": {
    "/v1/quote": {
      "post": {
        "tags": ["Quoting"],
        "summary": "Price one swap",
        "description": "Returns the best route and what it pays. Read `amountOutAfterFee` as the user's proceeds and `minAmountOut` as the floor to pass to `/v1/build`.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QuoteRequest" } } }
        },
        "responses": {
          "200": { "description": "Priced.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Quote" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/Unsatisfiable" },
          "429": { "description": "`rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/quotes": {
      "post": {
        "tags": ["Quoting"],
        "summary": "Price the same pair at several sizes",
        "description": "One round trip for a whole ladder. Takes `amountsIn: [...]` in place of `amountIn`.\n\n**A failed size does not fail the batch.** Each entry carries either `quote` or `error` + `message`, so a ladder whose top rung has no depth still returns the rungs below it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["chainId", "tokenIn", "tokenOut", "amountsIn"],
                "properties": {
                  "chainId": { "type": "integer", "example": 8453 },
                  "tokenIn": { "type": "string" },
                  "tokenOut": { "type": "string" },
                  "amountsIn": { "type": "array", "items": { "type": "string" } },
                  "slippageBps": { "type": "integer" },
                  "maxHops": { "type": "integer" },
                  "allowSplit": { "type": "boolean" },
                  "allowPartialFill": { "type": "boolean" },
                  "limitRate": { "$ref": "#/components/schemas/LimitRate" },
                  "maxImpactBps": { "type": "integer" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One entry per size, in order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "amountIn": { "type": "string" },
                          "quote": { "$ref": "#/components/schemas/Quote" },
                          "error": { "type": "string" },
                          "message": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/Unsatisfiable" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/build": {
      "post": {
        "tags": ["Quoting"],
        "summary": "Calldata for a quoted swap",
        "description": "**`minAmountOut` is not re-derived here.** It is the only protection the contract enforces, and a stale one is how a swap reverts after the user signed it — pass the value from a fresh quote.\n\n`maxFeeBps` bounds the protocol fee the contract may take: a deploy that raised the fee past your bound reverts rather than charging it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["chainId", "tokenIn", "tokenOut", "amountIn", "minAmountOut", "recipient"],
                "properties": {
                  "chainId": { "type": "integer", "example": 8453 },
                  "tokenIn": { "type": "string" },
                  "tokenOut": { "type": "string" },
                  "amountIn": { "type": "string" },
                  "minAmountOut": { "type": "string" },
                  "recipient": { "type": "string" },
                  "deadline": { "type": "integer", "description": "Unix seconds." },
                  "maxFeeBps": { "type": "integer" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sign and send this.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chainId": { "type": "integer" },
                    "action": { "type": "string" },
                    "to": { "type": "string" },
                    "data": { "type": "string" },
                    "value": { "type": "string", "description": "Non-zero only when `nativeIn` is true." },
                    "deadline": { "type": "integer" },
                    "maxFeeBps": { "type": "integer" },
                    "nativeIn": { "type": "boolean" },
                    "nativeOut": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/Unsatisfiable" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/sizing": {
      "get": {
        "tags": ["Quoting"],
        "summary": "How much fits under a cost bound",
        "description": "\"How much can I trade before it costs more than N bps?\" Answers the sizing question without a quote per candidate size.",
        "parameters": [
          { "name": "chainId", "in": "query", "required": true, "schema": { "type": "integer" }, "example": 8453 },
          { "name": "tokenIn", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "tokenOut", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "costBps", "in": "query", "schema": { "type": "string" }, "description": "Comma-separated bounds.", "example": "10,25,50" }
        ],
        "responses": {
          "200": { "description": "A level per bound, each with `costBps` and `avgCostBps`." },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/Unsatisfiable" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/spot": {
      "get": {
        "tags": ["Prices"],
        "summary": "Marginal rate for a pair",
        "description": "The price of an infinitesimal trade. **Use it for display — it is not a quote:** no size, no fee, no slippage.\n\nMakes no RPC call; served from the cached snapshot.",
        "parameters": [
          { "name": "chainId", "in": "query", "required": true, "schema": { "type": "integer" }, "example": 8453 },
          { "name": "base", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The token being priced." },
          { "name": "quote", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The token it is priced IN." }
        ],
        "responses": {
          "200": {
            "description": "The rate, plus where it came from.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chainId": { "type": "integer" },
                    "base": { "type": "string" },
                    "quote": { "type": "string" },
                    "price": { "type": "string", "description": "Decimal-adjusted." },
                    "priceRaw": { "type": "string" },
                    "quoteDecimals": { "type": "integer" },
                    "pool": { "type": "string" },
                    "via": { "type": "string", "description": "Present when routed through a connector." },
                    "liquidity": { "type": "string" },
                    "blockNumber": { "type": "integer" }
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/Unsatisfiable" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/stream/prices": {
      "get": {
        "tags": ["Prices"],
        "summary": "Live price ticks (SSE)",
        "description": "Server-sent events named `price`. Makes no RPC call.\n\n**`lagged` is the count of ticks this subscriber missed.** A non-zero value means the consumer is slower than the producer and has a gap — it is not decoration.",
        "parameters": [
          { "name": "chainId", "in": "query", "required": true, "schema": { "type": "integer" }, "example": 8453 },
          { "name": "pairs", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Comma-separated `base-quote` pairs.", "example": "0xA-0xB,0xC-0xD" }
        ],
        "responses": {
          "200": { "description": "An `text/event-stream` of `price` events carrying `chainId`, `blockNumber`, `prices[]` and `lagged`." },
          "422": { "$ref": "#/components/responses/Unsatisfiable" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/v1/tokens": {
      "get": {
        "tags": ["Discovery"],
        "summary": "Tokens this chain routes",
        "parameters": [
          { "name": "chainId", "in": "query", "required": true, "schema": { "type": "integer" }, "example": 8453 }
        ],
        "responses": {
          "200": { "description": "The curated token list for that chain." },
          "422": { "$ref": "#/components/responses/Unsatisfiable" }
        }
      }
    },
    "/v1/chains": {
      "get": {
        "tags": ["Discovery"],
        "summary": "Chains this instance serves",
        "description": "**Call this before assuming a chain is available.** A chain without an RPC configured is silently not served rather than fatal, so the registry listing it is not a guarantee this deployment answers for it.",
        "responses": {
          "200": { "description": "The chains with a configured RPC." }
        }
      }
    }
  }
}
