> ## Documentation Index
> Fetch the complete documentation index at: https://docs.olympex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Get a swap quote

> Quote a single-chain swap, convert the output to display units, add a gas estimate and keep fallback liquidity sources for the swap.

`POST /quotes` tells you how much of the output token a swap returns, which liquidity source offers the best route and, if you ask, what the transaction costs in gas. This guide quotes 10 USDT to USDC on Polygon. A quote is read-only: it moves no funds and reserves nothing.

<Info>
  You need an API key ID, secret key and passphrase. [Create a test API key](/get-started/create-an-api-key) gives you working credentials in one click. If your browser blocks the call, [create one from a terminal](/get-started/create-an-api-key#create-a-key-from-a-terminal).
</Info>

## Prerequisites

* `OLYMPEX_API_KEY_ID`, `OLYMPEX_SECRET_KEY` and `OLYMPEX_PASSPHRASE` set in your server's environment. Quotes are signed requests, so signing happens on your server, never in a browser or mobile app.
* The signing helper for your language, saved next to your code. TypeScript needs Node.js 22.18 or later, which runs `.ts` files directly, in an ES module project (`npm pkg set type=module`); Python needs 3.8 or later with `requests`, installed in a virtual environment as the [quickstart](/get-started/quickstart) shows. [Sign requests](/authentication/sign-requests) explains every header and publishes two known-answer vectors.
* For the unit conversion and the gas price in TypeScript: viem 2 (`npm install viem`) and an RPC URL for the chain, as `POLYGON_RPC_URL` in this guide.

<Accordion title="Signing helpers: sign-request.ts, sign_request.py and sign-request.sh">
  <Tabs>
    <Tab title="TypeScript">
      ```ts sign-request.ts theme={null}
      // Olympex REST API: request signing (signature v2) and a minimal client.
      // Node.js 18+ (global fetch, node:crypto). Runs unchanged under Node's built-in type stripping.
      import { createHash, createHmac, randomBytes } from "node:crypto";

      export const OLYMPEX_BASE_URL = "https://api-rest.olympex.io/api/v1";
      export const SIGNATURE_VERSION = "OLPX-HMAC-SHA256-V2";

      export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
      export type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE";

      export interface OlympexCredentials {
        apiKeyId: string; // sent as x-api-key-id
        secretKey: string; // shown once when the account is created; never sent
        passphrase: string; // the account password; sent as x-passphrase
      }

      export interface SignedRequest {
        method: string;
        path: string; // the URL path, starting with /api/v1
        query: string; // the canonical query, "" when there is none
        bodyString: string;
        bodyHash: string;
        timestamp: string;
        nonce: string;
        message: string; // the HMAC input
        headers: Record<string, string>;
      }

      const sortKeysDeep = (value: unknown): unknown => {
        if (Array.isArray(value)) return value.map(sortKeysDeep);
        if (value !== null && typeof value === "object") {
          const record = value as Record<string, unknown>;
          const sorted: Record<string, unknown> = {};
          for (const key of Object.keys(record).sort()) sorted[key] = sortKeysDeep(record[key]);
          return sorted;
        }
        return value;
      };

      /** The exact string the server hashes: keys sorted at every level, no whitespace. Empty when there is no body. */
      export const canonicalBody = (body: Json | undefined): string =>
        body === undefined || body === null ? "" : JSON.stringify(sortKeysDeep(body));

      // encodeURIComponent leaves !'()* as they are; RFC 3986 percent-encodes them.
      const rfc3986 = (text: string): string =>
        encodeURIComponent(text).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);

      /** The query the server signs: parameters decoded ("+" is a space), sorted by key and then by value, re-encoded per RFC 3986 and joined with "&". Empty when there is no query. */
      export const canonicalQuery = (search: string): string =>
        [...new URLSearchParams(search)]
          .sort(([ak, av], [bk, bv]) => (ak < bk ? -1 : ak > bk ? 1 : av < bv ? -1 : av > bv ? 1 : 0))
          .map(([key, value]) => `${rfc3986(key)}=${rfc3986(value)}`)
          .join("&");

      /**
       * Builds the four signed headers for one request. `url` is the full URL you send, query included.
       * Pass `undefined` as the body for GET and DELETE. Sign every attempt again: nonces are single-use.
       */
      export const signRequest = (
        method: string,
        url: string,
        body: Json | undefined,
        credentials: OlympexCredentials,
        options: { timestamp?: string; nonce?: string } = {},
      ): SignedRequest => {
        const verb = method.toUpperCase();
        const { pathname: path, search } = new URL(url);
        const query = canonicalQuery(search);
        const bodyString = canonicalBody(body);
        const timestamp = options.timestamp ?? Math.floor(Date.now() / 1000).toString();
        const nonce = options.nonce ?? randomBytes(12).toString("hex");
        const bodyHash = createHash("sha256").update(bodyString, "utf8").digest("base64url");
        const message = [SIGNATURE_VERSION, timestamp, nonce, verb, path, query, bodyHash].join("\n");
        const signature = createHmac("sha256", credentials.secretKey).update(message, "utf8").digest("hex");
        return {
          method: verb,
          path,
          query,
          bodyString,
          bodyHash,
          timestamp,
          nonce,
          message,
          headers: {
            "content-type": "application/json",
            "x-api-key-id": credentials.apiKeyId,
            "x-value-info": Buffer.from(`${timestamp}\n${nonce}\n${bodyHash}`, "utf8").toString("base64"),
            "x-passphrase": credentials.passphrase,
            "x-signature": signature,
          },
        };
      };

      export class OlympexApiError extends Error {
        readonly status: number;
        readonly code: string;
        readonly details: unknown[];
        readonly requestId: string | undefined;

        constructor(status: number, code: string, message: string, details: unknown[] = [], requestId?: string) {
          super(message);
          this.name = "OlympexApiError";
          this.status = status;
          this.code = code;
          this.details = details;
          this.requestId = requestId;
        }
      }

      interface Envelope<T> {
        success?: boolean;
        data?: T;
        error?: { code: string; message: string; details?: unknown[] };
        meta?: { requestId?: string };
        message?: string; // gateway responses (401, 403, 429, 500, 503) carry only this field
      }

      export const credentialsFromEnv = (
        env: Record<string, string | undefined> = process.env,
      ): OlympexCredentials => {
        const { OLYMPEX_API_KEY_ID: apiKeyId, OLYMPEX_SECRET_KEY: secretKey, OLYMPEX_PASSPHRASE: passphrase } = env;
        if (!apiKeyId || !secretKey || !passphrase) {
          throw new Error("Set OLYMPEX_API_KEY_ID, OLYMPEX_SECRET_KEY and OLYMPEX_PASSPHRASE.");
        }
        return { apiKeyId, secretKey, passphrase };
      };

      /**
       * Signs and sends one request, then returns `data` or throws OlympexApiError.
       * POST and PATCH need a body; GET and DELETE take none and are signed over the empty string.
       * `path` may carry a query string, for example "/tokens?chainId=137". The signature covers the method, the path and the query.
       */
      export async function olympexRequest<T = unknown>(
        method: HttpMethod,
        path: string,
        body?: Json,
        credentials: OlympexCredentials = credentialsFromEnv(),
      ): Promise<T> {
        const hasBody = method === "POST" || method === "PATCH";
        if (hasBody && body === undefined) throw new TypeError(`${method} ${path} needs a body`);
        if (!hasBody && body !== undefined) throw new TypeError(`${method} requests take no body`);
        const url = `${OLYMPEX_BASE_URL}${path}`;
        const signed = signRequest(method, url, body, credentials);
        const response = await fetch(url, {
          method,
          headers: signed.headers,
          body: hasBody ? signed.bodyString : undefined, // send exactly the bytes that were hashed
          signal: AbortSignal.timeout(35_000),
        });
        const text = await response.text();
        let payload: Envelope<T> | undefined;
        try {
          payload = JSON.parse(text) as Envelope<T>;
        } catch {
          payload = undefined;
        }
        if (response.ok && payload?.success === true) return payload.data as T;
        if (payload?.error) {
          const { code, message, details = [] } = payload.error;
          throw new OlympexApiError(response.status, code, message, details, payload.meta?.requestId);
        }
        throw new OlympexApiError(response.status, `HTTP_${response.status}`, payload?.message ?? (text || response.statusText));
      }
      ```
    </Tab>

    <Tab title="Python">
      ```python sign_request.py theme={null}
      """Olympex REST API: request signing (signature v2) and a minimal client (Python 3.8+)."""
      import base64
      import hashlib
      import hmac
      import json
      import os
      import re
      import secrets
      import time
      from urllib.parse import quote, unquote_plus, urlsplit

      import requests

      OLYMPEX_BASE_URL = "https://api-rest.olympex.io/api/v1"
      SIGNATURE_VERSION = "OLPX-HMAC-SHA256-V2"
      _MAX_SAFE_INTEGER = 2**53 - 1  # the server parses JSON numbers as JavaScript doubles


      def _check_signable(value, where="body"):
          """Reject values whose JSON text would differ between Python and the server's JavaScript canonicalizer."""
          if value is None or isinstance(value, (bool, str)):
              return
          if isinstance(value, int):
              if abs(value) > _MAX_SAFE_INTEGER:
                  raise ValueError(f"{where}: integer outside +/-(2**53 - 1); send it as a string")
              return
          if isinstance(value, float):
              if value != value or value in (float("inf"), float("-inf")):
                  raise ValueError(f"{where}: NaN and Infinity are not valid JSON")
              return
          if isinstance(value, dict):
              for key, item in value.items():
                  # JavaScript orders integer-like keys first, sorts by UTF-16 code units and drops "__proto__".
                  if not isinstance(key, str) or not key.isascii() or key.isdigit() or key == "__proto__":
                      raise ValueError(f"{where}: object keys must be non-numeric ASCII strings, got {key!r}")
                  _check_signable(item, f"{where}.{key}")
              return
          if isinstance(value, (list, tuple)):
              for index, item in enumerate(value):
                  _check_signable(item, f"{where}[{index}]")
              return
          raise TypeError(f"{where}: unsupported type {type(value).__name__}")


      def _js_number(value):
          """Format a float the way JavaScript's JSON.stringify does (ECMAScript Number::toString): 1.0 is "1", 1e21 is "1e+21"."""
          if value == 0:
              return "0"  # also -0.0
          mantissa, _, exponent = repr(abs(value)).partition("e")  # repr gives the shortest round-trip digits, like JavaScript
          whole, _, fraction = mantissa.partition(".")
          raw = whole + fraction
          digits = raw.lstrip("0")
          point = len(whole) + int(exponent or 0) - (len(raw) - len(digits))  # value = 0.<digits> * 10**point
          digits = digits.rstrip("0")
          if len(digits) <= point <= 21:
              text = digits + "0" * (point - len(digits))
          elif 0 < point <= 21:
              text = digits[:point] + "." + digits[point:]
          elif -6 < point <= 0:
              text = "0." + "0" * -point + digits
          else:
              exp = point - 1
              text = digits[0] + ("." + digits[1:] if len(digits) > 1 else "") + ("e+" if exp >= 0 else "e-") + str(abs(exp))
          return ("-" if value < 0 else "") + text


      def _canonical(value):
          if value is None:
              return "null"
          if isinstance(value, bool):
              return "true" if value else "false"
          if isinstance(value, int):
              return str(value)
          if isinstance(value, float):
              return _js_number(value)
          if isinstance(value, str):
              return json.dumps(value, ensure_ascii=False)
          if isinstance(value, dict):
              return "{" + ",".join(f"{json.dumps(key, ensure_ascii=False)}:{_canonical(value[key])}" for key in sorted(value)) + "}"
          return "[" + ",".join(_canonical(item) for item in value) + "]"


      def canonical_body(body):
          """The exact string the server hashes: keys sorted at every level, no whitespace, raw UTF-8,
          numbers formatted as JavaScript formats them. Empty when there is no body."""
          if body is None:
              return ""
          _check_signable(body)
          return _canonical(body)


      def canonical_query(query):
          """The query the server signs: parameters decoded ("+" is a space), sorted by key and then by value,
          re-encoded per RFC 3986 and joined with "&". Empty when there is no query."""
          pairs = []
          for part in query.split("&"):
              if part:
                  key, _, value = part.partition("=")
                  pairs.append((unquote_plus(key), unquote_plus(value)))
          # JavaScript compares strings by UTF-16 code units, which is the order of their UTF-16-BE bytes.
          pairs.sort(key=lambda pair: (pair[0].encode("utf-16-be"), pair[1].encode("utf-16-be")))
          return "&".join(f"{quote(key, safe='')}={quote(value, safe='')}" for key, value in pairs)


      def sign_request(method, url, body, credentials, timestamp=None, nonce=None):
          """Return the canonical body and the signed headers for one request. `url` is the full URL you send,
          query included. Pass body=None for GET and DELETE. Sign every attempt again: nonces are single-use."""
          method = method.upper()
          parts = urlsplit(url)
          path, query = parts.path, canonical_query(parts.query)
          body_string = canonical_body(body)
          timestamp = str(int(time.time()) if timestamp is None else timestamp)
          nonce = secrets.token_hex(12) if nonce is None else nonce
          if not re.fullmatch(r"[0-9a-fA-F]{24}", nonce):
              raise ValueError("nonce must be 24 hexadecimal characters")
          digest = hashlib.sha256(body_string.encode("utf-8")).digest()
          body_hash = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
          message = "\n".join([SIGNATURE_VERSION, timestamp, nonce, method, path, query, body_hash])
          # The HMAC key is the secret key string itself (UTF-8 bytes), not its base64url-decoded bytes.
          signature = hmac.new(credentials["secret_key"].encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
          value_info = f"{timestamp}\n{nonce}\n{body_hash}"
          return {
              "method": method,
              "path": path,
              "query": query,
              "body_string": body_string,
              "body_hash": body_hash,
              "timestamp": timestamp,
              "nonce": nonce,
              "message": message,
              "headers": {
                  "content-type": "application/json",
                  "x-api-key-id": credentials["api_key_id"],
                  "x-value-info": base64.b64encode(value_info.encode("utf-8")).decode("ascii"),
                  "x-passphrase": credentials["passphrase"],
                  "x-signature": signature,
              },
          }


      class OlympexApiError(Exception):
          def __init__(self, status, code, message, details=None, request_id=None):
              super().__init__(f"{status} {code}: {message}")
              self.status = status
              self.code = code
              self.message = message
              self.details = details or []
              self.request_id = request_id


      def credentials_from_env():
          try:
              return {
                  "api_key_id": os.environ["OLYMPEX_API_KEY_ID"],
                  "secret_key": os.environ["OLYMPEX_SECRET_KEY"],
                  "passphrase": os.environ["OLYMPEX_PASSPHRASE"],
              }
          except KeyError as missing:
              raise RuntimeError(f"Set {missing.args[0]} (and the other OLYMPEX_* variables).") from None


      def olympex_request(method, path, body=None, credentials=None, timeout=35):
          """Sign and send one request, then return `data` or raise OlympexApiError.

          POST and PATCH need a body; GET and DELETE take none and are signed over the empty string.
          `path` may carry a query string, for example "/tokens?chainId=137". The signature covers the method, the path and the query.
          """
          method = method.upper()
          has_body = method in ("POST", "PATCH")
          if has_body and body is None:
              raise ValueError(f"{method} {path} needs a body")
          if not has_body and body is not None:
              raise ValueError(f"{method} requests take no body")
          url = OLYMPEX_BASE_URL + path
          signed = sign_request(method, url, body, credentials or credentials_from_env())
          response = requests.request(
              method,
              url,
              # bytes: http.client sends a str body as Latin-1
              data=signed["body_string"].encode("utf-8") if has_body else None,
              headers=signed["headers"],
              timeout=timeout,
          )
          try:
              payload = response.json()
          except ValueError:
              payload = None
          if not isinstance(payload, dict):
              payload = {}
          if response.ok and payload.get("success") is True:
              return payload.get("data")
          error = payload.get("error")
          if isinstance(error, dict):
              raise OlympexApiError(
                  response.status_code,
                  error.get("code"),
                  error.get("message"),
                  error.get("details"),
                  (payload.get("meta") or {}).get("requestId"),
              )
          # Gateway responses (401, 403, 429, 500, 503) carry only {"message": ...}.
          raise OlympexApiError(response.status_code, f"HTTP_{response.status_code}", payload.get("message") or response.text)
      ```
    </Tab>

    <Tab title="Bash">
      ```bash sign-request.sh theme={null}
      # Olympex REST API: sign (signature v2) and send one request with openssl and curl.
      # Works in bash 3.2+ and zsh, with OpenSSL 1.1+/3.x or LibreSSL.
      # Needs OLYMPEX_API_KEY_ID, OLYMPEX_SECRET_KEY and OLYMPEX_PASSPHRASE in the environment.
      OLYMPEX_BASE_URL="https://api-rest.olympex.io/api/v1"

      # Usage: olympex_request METHOD PATH [BODY]
      #   olympex_request GET '/tokens?chainId=137'
      #   olympex_request POST /support-chain '{"chainId":137}'
      # POST and PATCH need a body that is already canonical JSON (keys sorted at every level, no whitespace):
      # it is hashed byte for byte and the server hashes its own canonical form.
      # GET and DELETE take no body and are signed over the empty string.
      # The signature also covers the method, the path (with its /api/v1 prefix) and the query. The query is signed as
      # written, so write it in canonical form: parameters sorted by key and then by value, each percent-encoded per RFC 3986.
      # OLYMPEX_TIMESTAMP and OLYMPEX_NONCE, if set, replace the current time and the random nonce. They exist only to
      # reproduce the known-answer vectors: never export them, or every later request fails with 403 (stale time, reused nonce).
      olympex_request() {
        local method="$1" endpoint="$2" body="${3-}" query=""
        local ts nonce body_hash sign_path value_info message signature
        : "${OLYMPEX_API_KEY_ID:?set OLYMPEX_API_KEY_ID}" "${OLYMPEX_SECRET_KEY:?set OLYMPEX_SECRET_KEY}" "${OLYMPEX_PASSPHRASE:?set OLYMPEX_PASSPHRASE}"
        case "$method" in
          POST|PATCH) [ -n "$body" ] || { printf 'olympex_request: %s needs a body\n' "$method" >&2; return 2; } ;;
          GET|DELETE) [ -z "$body" ] || { printf 'olympex_request: %s takes no body\n' "$method" >&2; return 2; } ;;
          *) printf 'olympex_request: unsupported method %s\n' "$method" >&2; return 2 ;;
        esac
        # Never name a variable `path`: zsh ties it to PATH.
        sign_path="/${OLYMPEX_BASE_URL#https://*/}${endpoint%%\?*}"
        case "$endpoint" in *\?*) query="${endpoint#*\?}" ;; esac
        ts="${OLYMPEX_TIMESTAMP:-$(date +%s)}"
        nonce="${OLYMPEX_NONCE:-$(openssl rand -hex 12)}"
        body_hash="$(printf '%s' "$body" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
        value_info="$(printf '%s\n%s\n%s' "$ts" "$nonce" "$body_hash" | openssl base64 -A)"
        message="$(printf 'OLPX-HMAC-SHA256-V2\n%s\n%s\n%s\n%s\n%s\n%s' "$ts" "$nonce" "$method" "$sign_path" "$query" "$body_hash")"
        signature="$(printf '%s' "$message" | openssl dgst -sha256 -hmac "$OLYMPEX_SECRET_KEY" -binary | od -An -v -tx1 | tr -d ' \n')"
        curl -sS --max-time 35 -X "$method" "$OLYMPEX_BASE_URL$endpoint" \
          -H "content-type: application/json" \
          -H "x-api-key-id: $OLYMPEX_API_KEY_ID" \
          -H "x-value-info: $value_info" \
          -H "x-passphrase: $OLYMPEX_PASSPHRASE" \
          -H "x-signature: $signature" \
          ${body:+--data-raw} ${body:+"$body"}
      }
      ```
    </Tab>
  </Tabs>
</Accordion>

## Steps

<Steps>
  <Step title="Choose the chain and tokens">
    Set `chainId` to the EVM chain ID as an **integer**, for example `137` for Polygon. Olympex supports Ethereum (`1`), Optimism (`10`), BNB Chain (`56`), Polygon (`137`), Base (`8453`), Arbitrum (`42161`), Avalanche (`43114`) and Linea (`59144`). [`GET /chains`](/api-reference/chains/list-chains) returns the enabled chain IDs, without names: keep the names in your code, and offer a chain only while its ID is in the response. See [Supported chains](/concepts/supported-chains).

    Token addresses can be lowercase or EIP-55 checksummed. For the chain's native token (POL on Polygon, ETH on Ethereum), use `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. `GET /tokens` lists it in lowercase, `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`, and quotes accept either casing, so compare addresses case-insensitively.

    Olympex resolves token decimals itself, but you need the output token's decimals to display the result. Take `decimals` from [`GET /tokens`](/api-reference/tokens/list-tokens), which returns the tokens Olympex lists on a chain, or read `decimals()` from the token contract. The list doesn't limit what you can quote: any valid token address works, listed or not, and the quote tells you whether it has a route. This guide uses:

    | Token | Address | Decimals |
    | - | - | - |
    | USDT on Polygon | `0xc2132d05d31c914a87c6611c10748aeb04b58e8f` | 6 |
    | USDC on Polygon | `0x3c499c542cef5e3811e1192ce70d8cc03d5c3359` | 6 |
  </Step>

  <Step title="Build the request body">
    ```json theme={null}
    {
      "mode": "single-chain",
      "params": {
        "chainId": 137,
        "inTokenAddress": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
        "outTokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
        "amount": "10",
        "slippage": "1",
        "gasPrice": "35"
      }
    }
    ```

    | Field | What to send |
    | - | - |
    | `mode` | `"single-chain"`. A swap between two chains uses other fields; see [Cross-chain swap end-to-end](/guides/cross-chain-swap-end-to-end). |
    | `params.amount` | The input amount in **human-readable** units, as a decimal string. `"10"` is 10 USDT. Don't convert it to base units. |
    | `params.slippage` | The maximum slippage in percent, as a string. `"1"` is 1%. See [Slippage and price impact](/concepts/slippage-and-price-impact). |
    | `params.gasPrice` | Required. A gas price hint in whole gwei, as a string such as `"35"`. Some liquidity sources use it. With `includeGasInfo`, `dataFeeTransaction.effectiveGasPrice` reports the gas price the estimate used, in wei. |

    Optional fields tune the result:

    | Field | Effect |
    | - | - |
    | `params.includeGasInfo` | `true` adds `dataFeeTransaction`, a gas fee estimate. Default `false`. |
    | `params.orderBy` | How routes are ranked: `MAX_OUT_AMOUNT` (default, best output), `MIN_ESTIMATE_GAS` or `MAX_ESTIMATE_GAS`. |
    | `params.gasMultiplier` | Scales `estimatedGas` when `includeGasInfo` is `true`: `NONE` (1×, default), `LOW` (1.55×), `MEDIUM` (2×) or `HIGH` (4×). |
    | `params.excludeMetaAggregatorId` | Liquidity sources to leave out, for example `["zeroExV2AllowanceHolder"]`. Unknown IDs are ignored. See [Aggregation and routing](/concepts/aggregation-and-routing#excluding-sources). |
    | `fees` | Your integrator fee, at the top level next to `params`: `feeBps` from 0 to 100 (100 is 1%) and the `feeRecipient` address. See [Gas and fees](/concepts/gas-and-fees#integrator-fees). |

    Send `amount`, `slippage` and `gasPrice` as decimal strings. A JSON number in any of them returns `400 VALIDATION_ERROR`, as the example under [Verify](#verify) shows.

    <Tip>
      Send the chain's current gas price as the hint instead of a constant, as a string of whole gwei rounded up, for example `"36"`. Some sources reject fractional gwei: a hint such as `"35.2"` makes the `openOceanV3` source fail, and the quote silently leaves it out. On a chain whose gas price is below 1 gwei, rounding up gives `"1"`, above the real price. The hint doesn't set your transaction's gas price.
    </Tip>
  </Step>

  <Step title="Send the request">
    Each call signs the body with a new timestamp and nonce. Olympex compares several liquidity sources for every quote, so a response can take several seconds; the helpers wait up to 35 seconds, a little longer than the gateway timeout of about 30 seconds. The TypeScript snippets in the next steps continue `get-swap-quote.ts`.

    <CodeGroup>
      ```bash cURL theme={null}
      # Single-chain quote: 10 USDT to USDC on Polygon.
      # Needs openssl, curl and OLYMPEX_API_KEY_ID, OLYMPEX_SECRET_KEY, OLYMPEX_PASSPHRASE.
      METHOD=POST
      ENDPOINT=/quotes
      QUERY=''
      BODY='{"mode":"single-chain","params":{"amount":"10","chainId":137,"gasPrice":"35","inTokenAddress":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","outTokenAddress":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","slippage":"1"}}'
      TS="${OLYMPEX_TIMESTAMP:-$(date +%s)}"
      NONCE="${OLYMPEX_NONCE:-$(openssl rand -hex 12)}"
      BODY_HASH="$(printf '%s' "$BODY" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
      VALUE_INFO="$(printf '%s\n%s\n%s' "$TS" "$NONCE" "$BODY_HASH")"
      MESSAGE="$(printf 'OLPX-HMAC-SHA256-V2\n%s\n%s\n%s\n%s\n%s\n%s' "$TS" "$NONCE" "$METHOD" "/api/v1$ENDPOINT" "$QUERY" "$BODY_HASH")"
      curl -sS -X "$METHOD" "https://api-rest.olympex.io/api/v1$ENDPOINT${QUERY:+?$QUERY}" \
        -H "content-type: application/json" \
        -H "x-api-key-id: $OLYMPEX_API_KEY_ID" \
        -H "x-value-info: $(printf '%s' "$VALUE_INFO" | openssl base64 -A)" \
        -H "x-passphrase: $OLYMPEX_PASSPHRASE" \
        -H "x-signature: $(printf '%s' "$MESSAGE" | openssl dgst -sha256 -hmac "$OLYMPEX_SECRET_KEY" -binary | od -An -v -tx1 | tr -d ' \n')" \
        --data-raw "$BODY"
      ```

      ```ts TypeScript theme={null}
      // get-swap-quote.ts. Run: node get-swap-quote.ts
      import { olympexRequest } from "./sign-request.ts"; // /authentication/sign-requests

      type SingleChainQuote = {
        outAmount: string; // base units of the output token
        aggregatorId: string;
        aggregatorOrder: string[] | null;
        estimatedGas: string | null;
      };
      type QuoteData = { mode: "single-chain"; quote: SingleChainQuote };

      const { quote } = await olympexRequest<QuoteData>("POST", "/quotes", {
        mode: "single-chain",
        params: {
          chainId: 137,
          inTokenAddress: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", // USDT
          outTokenAddress: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", // USDC
          amount: "10",
          slippage: "1",
          gasPrice: "35",
        },
      });
      console.log(quote.aggregatorId, quote.outAmount); // oneInch 9979975
      ```

      ```python Python theme={null}
      from sign_request import olympex_request  # /authentication/sign-requests

      data = olympex_request("POST", "/quotes", {
          "mode": "single-chain",
          "params": {
              "chainId": 137,
              "inTokenAddress": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",  # USDT
              "outTokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",  # USDC
              "amount": "10",
              "slippage": "1",
              "gasPrice": "35",
          },
      })
      quote = data["quote"]
      print(quote["aggregatorId"], quote["outAmount"])  # oneInch 9979975
      ```
    </CodeGroup>

    <Tip>
      To try the request without writing code, use the console on [Get a quote](/api-reference/quotes/get-quote). It signs in your browser, so use a test key there and keep production credentials on your servers. If the browser blocks the call, [Copy as cURL](/api-reference/console#copy-as-curl) runs the same request from a terminal.
    </Tip>
  </Step>

  <Step title="Read the response">
    The quote is in `data.quote` of the standard envelope:

    | Field | Meaning |
    | - | - |
    | `outAmount` | The expected output, as an integer string in **base units of the output token**. |
    | `aggregatorId` | The liquidity source with the best route. Pass it to `POST /swap`. |
    | `aggregatorOrder` | Every source that returned a quote, best first. |
    | `estimatedGas` | Gas units estimated by the source for its part of the route. It doesn't include the Olympex contracts, so the transaction uses more: estimate its gas yourself before you send. `"0"` means the source gave no estimate. |
    | `routes` | The route breakdown the source reports. Display only. Each route's `percentage` is the source's own figure and can be fractional; the values don't always sum to 100, because later hops can appear as separate routes at 100. `subRoutes` gives each hop's `from` and `to` token addresses as the source reports them (case and the native-token address vary by source) and the DEXes used. |
    | `market` | Other venues' output for the same trade (`dexName`, `swapAmount` in base units, `dexImageURL`). Can be empty. |
    | `integratorFeeBreakdown` | The Olympex protocol fee and your integrator fee, on every quote. `protocolFeeBps` is in basis points (`15` is 0.15%) and applies even when you send no `fees`; `integratorMarginBps` is then `0`. The amounts are in base units of the output token. Some liquidity sources also charge their own fee inside the route: it is already deducted from `outAmount` and isn't part of this breakdown. |

    `meta.requestId` identifies the request. Log it: support needs it to find the request.

    If no source can quote the pair and amount, the call fails with `422 NO_ROUTE`. It can be temporary: retry with backoff, then try another amount or pair. See [When no route is found](/concepts/aggregation-and-routing#when-no-route-is-found).

    The response below was requested with `includeGasInfo: true`, so it carries `dataFeeTransaction`.

    <Accordion title="Response">
      ```json theme={null}
      {
        "success": true,
        "data": {
          "mode": "single-chain",
          "quote": {
            "outAmount": "9979975",
            "estimatedGas": "1578462",
            "aggregatorId": "oneInch",
            "aggregatorOrder": [
              "oneInch",
              "uniswapV3Hermes"
            ],
            "market": [],
            "routes": [
              {
                "percentage": 90.23750000000001,
                "subRoutes": [
                  {
                    "from": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
                    "to": "0xa3fa99a148fa48d14ed51d610c367c61876997f1",
                    "dexes": [
                      {
                        "name": "POLYGON_BALANCER_V2",
                        "percentage": 100
                      }
                    ]
                  },
                  {
                    "from": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
                    "to": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
                    "dexes": [
                      {
                        "name": "POLYGON_UNISWAP_V4",
                        "percentage": 100
                      }
                    ]
                  }
                ]
              },
              {
                "percentage": 9.762500000000001,
                "subRoutes": [
                  {
                    "from": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
                    "to": "0xa3fa99a148fa48d14ed51d610c367c61876997f1",
                    "dexes": [
                      {
                        "name": "POLYGON_BALANCER_V2",
                        "percentage": 100
                      }
                    ]
                  },
                  {
                    "from": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
                    "to": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
                    "dexes": [
                      {
                        "name": "POLYGON_UNISWAP_V4",
                        "percentage": 100
                      }
                    ]
                  }
                ]
              },
              {
                "percentage": 100,
                "subRoutes": [
                  {
                    "from": "0xa3fa99a148fa48d14ed51d610c367c61876997f1",
                    "to": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6",
                    "dexes": [
                      {
                        "name": "POLYGON_QUICKSWAP_V3",
                        "percentage": 100
                      }
                    ]
                  }
                ]
              },
              {
                "percentage": 100,
                "subRoutes": [
                  {
                    "from": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6",
                    "to": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
                    "dexes": [
                      {
                        "name": "POLYGON_DODO_V2",
                        "percentage": 100
                      }
                    ]
                  }
                ]
              },
              {
                "percentage": 100,
                "subRoutes": [
                  {
                    "from": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
                    "to": "0xac0f66379a6d7801d7726d5a943356a172549adb",
                    "dexes": [
                      {
                        "name": "POLYGON_QUICKSWAP",
                        "percentage": 100
                      }
                    ]
                  }
                ]
              },
              {
                "percentage": 100,
                "subRoutes": [
                  {
                    "from": "0xac0f66379a6d7801d7726d5a943356a172549adb",
                    "to": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
                    "dexes": [
                      {
                        "name": "POLYGON_UNISWAP_V3",
                        "percentage": 100
                      }
                    ]
                  }
                ]
              }
            ],
            "dataFeeTransaction": {
              "effectiveGasPrice": "385800996250",
              "transactionFee": "608972212142767500",
              "transactionFeeInUSD": "0.074812",
              "transactionFeeInToken": "0.074812",
              "valueToApprove": "10.074812",
              "nativePrice": "0.12285",
              "tokenPrice": "1"
            },
            "gasMultiplier": "NONE",
            "integratorFeeBreakdown": {
              "protocolFeeBps": 15,
              "integratorMarginBps": 0,
              "protocolFeeAmount": "14969",
              "integratorMarginAmount": "0"
            }
          }
        },
        "meta": {
          "requestId": "EdtwKiuLoAMEPYQ=",
          "version": "v1",
          "accountType": "integrator",
          "apiKeyId": "00000000-0000-4000-8000-000000000000"
        }
      }
      ```
    </Accordion>
  </Step>

  <Step title="Convert the output to display units">
    `outAmount` is in base units of the output token. Divide it by 10 to the power of that token's decimals: `"9979975"` of 6-decimal USDC is 9.979975 USDC.

    <CodeGroup>
      ```ts TypeScript theme={null}
      import { createPublicClient, erc20Abi, formatUnits, http } from "viem";
      import { polygon } from "viem/chains";

      const USDC = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359";
      const client = createPublicClient({ chain: polygon, transport: http(process.env.POLYGON_RPC_URL) });

      const decimals = await client.readContract({ address: USDC, abi: erc20Abi, functionName: "decimals" });
      console.log(`${formatUnits(BigInt(quote.outAmount), decimals)} USDC`); // 9.979975 USDC
      ```

      ```python Python theme={null}
      from decimal import Decimal

      OUT_DECIMALS = 6  # USDC on Polygon

      received = Decimal(quote["outAmount"]).scaleb(-OUT_DECIMALS)
      print(f"{received} USDC")  # 9.979975 USDC
      ```
    </CodeGroup>

    Keep amounts as `bigint` or `Decimal` until you format them for display. Floating-point arithmetic loses precision on 18-decimal tokens.
  </Step>

  <Step title="Keep fallback sources for the swap">
    `aggregatorOrder` lists every source that quoted the pair, best first, and can be `null`. Build the list you'll try with `POST /swap`: the winner first, then the rest.

    ```ts theme={null}
    const sources = [quote.aggregatorId, ...(quote.aggregatorOrder ?? []).filter((id) => id !== quote.aggregatorId)];
    // ["oneInch", "uniswapV3Hermes"]
    ```

    If `POST /swap` returns `422 NO_ROUTE` or `500 SWAP_ERROR` for one source, try the next. Do the same when `POST /swap` succeeds but `eth_estimateGas` of its calldata reverts: a `200` doesn't guarantee that the calldata executes. A fallback source can return less than the winner, so show the user the `outAmount` and `minOutAmount` from the `POST /swap` response, not the quote's. [Execute a swap](/guides/execute-a-swap) uses this list, and [Aggregation and routing](/concepts/aggregation-and-routing#falling-back-with-aggregatororder) explains the ranking.
  </Step>

  <Step title="Add the gas fee estimate (optional)">
    Set `includeGasInfo: true` to receive `dataFeeTransaction` in the quote. This request also sends the chain's current gas price as the hint:

    <CodeGroup>
      ```ts TypeScript theme={null}
      const GWEI = 1_000_000_000n;

      type DataFeeTransaction = {
        transactionFee: string; // wei
        transactionFeeInUSD: string;
        valueToApprove: string; // human-readable units of the input token
      };
      type QuoteWithGas = { quote: SingleChainQuote & { dataFeeTransaction?: DataFeeTransaction } };

      const withGas = await olympexRequest<QuoteWithGas>("POST", "/quotes", {
        mode: "single-chain",
        params: {
          chainId: 137,
          inTokenAddress: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
          outTokenAddress: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
          amount: "10",
          slippage: "1",
          gasPrice: (((await client.getGasPrice()) + GWEI - 1n) / GWEI).toString(), // whole gwei, rounded up: for example "36"
          includeGasInfo: true,
        },
      });
      console.log(`Gas fee: about $${withGas.quote.dataFeeTransaction?.transactionFeeInUSD}`);
      ```

      ```python Python theme={null}
      data = olympex_request("POST", "/quotes", {
          "mode": "single-chain",
          "params": {
              "chainId": 137,
              "inTokenAddress": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
              "outTokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
              "amount": "10",
              "slippage": "1",
              "gasPrice": "35",  # better: the chain's current gas price from your RPC, in whole gwei, rounded up
              "includeGasInfo": True,
          },
      })
      fee = data["quote"].get("dataFeeTransaction") or {}
      print(f"Gas fee: about ${fee.get('transactionFeeInUSD')}")
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "dataFeeTransaction": {
        "effectiveGasPrice": "385800996250",
        "transactionFee": "608972212142767500",
        "transactionFeeInUSD": "0.074812",
        "transactionFeeInToken": "0.074812",
        "valueToApprove": "10.074812",
        "nativePrice": "0.12285",
        "tokenPrice": "1"
      }
    }
    ```

    | Field | Unit |
    | - | - |
    | `effectiveGasPrice` | Wei per gas unit: the gas price used for the estimate. |
    | `transactionFee` | Wei of the native token: `estimatedGas × effectiveGasPrice`. |
    | `transactionFeeInUSD` | USD. |
    | `transactionFeeInToken` | The fee converted to the input token, in human-readable units. |
    | `valueToApprove` | `amount` plus `transactionFeeInToken`, in human-readable units of the input token. |
    | `nativePrice`, `tokenPrice` | USD prices of the native token and of the input token. |

    `gasMultiplier` scales `estimatedGas` when `includeGasInfo` is `true`. `dataFeeTransaction` derives from the source's `estimatedGas`, which leaves out the Olympex contracts, so treat it as a lower bound. The fee your user pays depends on the gas used and the gas price when the transaction is mined, so present this value as an estimate, and add a buffer when you size an allowance from `valueToApprove` or `transactionFeeInToken`. [Gas and fees](/concepts/gas-and-fees#gas-estimates-in-a-quote) covers every gas field.
  </Step>
</Steps>

## Verify

* The response has `"success": true`, and `data.mode` is `"single-chain"`.
* `outAmount`, converted with the output token's decimals, is close to the input amount for a stablecoin pair: 10 USDT quoted 9.979975 USDC in the example above.
* A rejected body returns `400 VALIDATION_ERROR`, and `error.details` names each field to fix. This one sent `chainId` as a string, a malformed token address, `amount` as a number and no `gasPrice`:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": [
      {
        "field": "params.chainId",
        "message": "Invalid input: expected number, received string"
      },
      {
        "field": "params.inTokenAddress",
        "message": "Must be a valid EVM address"
      },
      {
        "field": "params.amount",
        "message": "Invalid input: expected string, received number"
      },
      {
        "field": "params.gasPrice",
        "message": "Invalid input: expected string, received undefined"
      }
    ]
  },
  "meta": {
    "requestId": "ENUlmg3dIAMEMEw=",
    "version": "v1",
    "accountType": "integrator",
    "apiKeyId": "00000000-0000-4000-8000-000000000000"
  }
}
```

## Common pitfalls

<Warning>
  **Base units in `amount`.** `amount` is human-readable. Sending `"10000000"` to mean 10 USDT quotes ten million USDT, and a swap built from it asks the wallet for that amount.
</Warning>

<Warning>
  **The wrong decimals for `outAmount`.** Convert `outAmount` with the output token's decimals, not the input token's. For a USDT (6 decimals) to WETH (18 decimals) swap, the wrong choice is off by a factor of 10¹².
</Warning>

<Warning>
  **Stale quotes.** A quote is not reserved, and prices move between the quote and the swap. Request `POST /swap` right after the quote, and quote again if the user waits before confirming.
</Warning>

<Warning>
  **`chainId` as a string.** Every endpoint takes chain IDs as JSON integers (`137`). A string such as `"137"` returns `400 VALIDATION_ERROR`.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="Execute a swap" icon="arrows-rotate" href="/guides/execute-a-swap">
    Turn the quote into calldata and send it from a wallet.
  </Card>

  <Card title="Get a quote" icon="code" href="/api-reference/quotes/get-quote">
    Every field of `POST /quotes`, with a console to try it.
  </Card>

  <Card title="Aggregation and routing" icon="route" href="/concepts/aggregation-and-routing">
    How Olympex compares liquidity sources.
  </Card>

  <Card title="Gas and fees" icon="magnifying-glass-dollar" href="/concepts/gas-and-fees">
    Gas estimates, protocol fees and integrator fees.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.