> ## 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.

# Quickstart

> Create a test API key and send your first signed requests to the Olympex REST API in minutes.

This quickstart takes you from no credentials to a signed quote for 10 USDT to USDC on Polygon. You create a test API key, save a reference signer, list the chains Olympex has enabled, and request a quote. To try the same requests without a terminal, use the [API console](/api-reference/console), which signs them in your browser.

<Info>
  You need a terminal and one of bash or zsh, Node.js 22.18 or later, or Python 3.8 or later. You don't need a wallet or tokens: nothing in this quickstart signs or sends a transaction, or creates an order.
</Info>

## Prerequisites

Pick one language and follow its tab in each step.

| Language | Requirements |
| - | - |
| Shell | bash 3.2+ or zsh, with `curl` and `openssl` (OpenSSL 1.1+, 3.x or LibreSSL). macOS and most Linux distributions include all three. |
| TypeScript | Node.js 22.18 or later, which runs `.ts` files directly with no build step. The files are ES modules: run them in a folder with no `package.json`, or in a project whose `package.json` has `"type": "module"`. `npm init -y` writes `"type": "commonjs"`, so run `npm pkg set type=module` after it. |
| Python | Python 3.8 or later and the `requests` package, installed in a virtual environment. |

## Steps

<Steps>
  <Step title="Create a test API key">
    Create a key with the console below. It creates a real API account on the live Olympex API and shows three values: the API key ID, the secret key (a random string) and a generated passphrase. Copy all three now: Olympex can't show the secret key or the passphrase again.

    Create the key from a terminal with a public `POST /accounts` request, as described in [Create a test API key](/get-started/create-an-api-key#create-a-key-from-a-terminal).

    The console also loads the new key into every [API console](/api-reference/console) on this site until you reload the page. If your browser blocks the call, [create the key from a terminal](/get-started/create-an-api-key#create-a-key-from-a-terminal) instead.

    Export the three values in the terminal you use for the next steps. The signers read them from the environment.

    ```bash theme={null}
    # Replace each placeholder with the value the console shows you.
    export OLYMPEX_API_KEY_ID="your-api-key-id"
    export OLYMPEX_SECRET_KEY="your-secret-key"
    export OLYMPEX_PASSPHRASE="your-passphrase"
    ```

    <Tip>
      Downloaded the `olympex.env` file from the console instead? Load it with `set -a; . ./olympex.env; set +a`, which exports every variable in the file without typing a secret into your shell history.
    </Tip>
  </Step>

  <Step title="Save the reference signer">
    Save the signer for your language next to your code. Each one takes the method, the path and, for `POST` and `PATCH`, the body. It signs the request with your secret key and sends it with the four signed headers. The TypeScript and Python signers canonicalize the JSON body; the shell function expects a body that is already canonical (keys sorted at every level, no whitespace). [Sign requests](/authentication/sign-requests) explains each step of the algorithm.

    <Tabs>
      <Tab title="Shell">
        ```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"}
        }
        ```

        Load the function into the same terminal:

        ```bash theme={null}
        source ./sign-request.sh
        ```
      </Tab>

      <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));
        }
        ```

        The file has no dependencies beyond Node.js. It is an ES module: save it in a folder with no `package.json`, or in a project whose `package.json` has `"type": "module"`.
      </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)
        ```

        Install its one dependency in a virtual environment. Homebrew's Python and recent Debian and Ubuntu releases refuse a system-wide `pip install` with `error: externally-managed-environment`:

        ```bash theme={null}
        python3 -m venv .venv
        . .venv/bin/activate
        python3 -m pip install requests
        ```

        Run the Python steps below in the same terminal, with the environment active. On Debian and Ubuntu, install the `python3-venv` package first if `python3 -m venv` fails.
      </Tab>
    </Tabs>
  </Step>

  <Step title="List the enabled chains">
    Start with a request that has no body. [`GET /chains`](/api-reference/chains/list-chains) returns the IDs of the chains Olympex has enabled. A `GET` sends no body, so the signer signs the empty string.

    <CodeGroup>
      ```bash Shell theme={null}
      olympex_request GET /chains
      ```

      ```ts TypeScript theme={null}
      // list-chains.ts. Run: node list-chains.ts
      import { olympexRequest } from "./sign-request.ts";

      const { chainIds } = await olympexRequest<{ chainIds: number[] }>("GET", "/chains");
      console.log(chainIds.includes(137) ? "Polygon is enabled" : "Polygon is not enabled");
      ```

      ```python Python theme={null}
      # list_chains.py. Run: python3 list_chains.py
      from sign_request import olympex_request

      chain_ids = olympex_request("GET", "/chains")["chainIds"]
      print("Polygon is enabled" if 137 in chain_ids else "Polygon is not enabled")
      ```
    </CodeGroup>

    The shell function prints the full response envelope. `chainIds` lists every chain Olympex has enabled, in ascending numeric order:

    ```json theme={null}
    {
      "success": true,
      "data": {
        "chainIds": [
          1,
          10,
          56,
          137,
          8453,
          42161,
          43114,
          59144
        ]
      },
      "meta": {
        "requestId": "E7eeEiPDIAMEP7Q=",
        "version": "v1",
        "accountType": "integrator",
        "apiKeyId": "00000000-0000-4000-8000-000000000000"
      }
    }
    ```

    The chain IDs are integers, the same type every endpoint takes. `meta.apiKeyId` is the API key ID that signed the request. The TypeScript and Python helpers return `data` directly, and raise `OlympexApiError` with the HTTP status, `error.code` and `meta.requestId` when the call fails. A gateway response, such as a `401` or `403` from a signing problem, has no `meta.requestId`, so the helpers raise it without a request ID. Its ID is in the `apigw-requestid` response header: log that header from your HTTP client, or add `-i` to a cURL command to see it.

    Two related calls work the same way. [`GET /tokens?chainId=137`](/api-reference/tokens/list-tokens) returns the tokens Olympex lists on Polygon, with each token's `address`, `symbol` and `decimals`: in the shell, quote the path (`olympex_request GET '/tokens?chainId=137'`) because zsh treats `?` as a wildcard. [`POST /support-chain`](/api-reference/chains/check-chain-support) checks a single chain ID, sent as an integer, against the same list.
  </Step>

  <Step title="Get a quote">
    Request the best route for 10 USDT to USDC on Polygon with 1% slippage.

    <CodeGroup>
      ```bash Shell theme={null}
      # 10 USDT to USDC on Polygon. The body is canonical JSON: keys sorted, no whitespace.
      olympex_request POST /quotes '{"mode":"single-chain","params":{"amount":"10","chainId":137,"gasPrice":"35","inTokenAddress":"0xc2132d05d31c914a87c6611c10748aeb04b58e8f","outTokenAddress":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","slippage":"1"}}'
      ```

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

      type QuoteData = { mode: "single-chain"; quote: { aggregatorId: string; outAmount: string } };

      const data = 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(data.quote.aggregatorId, data.quote.outAmount);
      ```

      ```python Python theme={null}
      # get_quote.py. Run: python3 get_quote.py
      from sign_request import olympex_request

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

    What each field means:

    * `mode`: `single-chain` for a swap on one chain. A transfer between two chains uses `cross-chain` and different `params`.
    * `chainId`: the chain as an **integer** (`137`), as on every endpoint.
    * `inTokenAddress`, `outTokenAddress`: the token you sell and the token you buy. Use `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` for the chain's native token.
    * `amount`: how much you sell, as a human-readable decimal string. `"10"` is 10 USDT; don't convert it to base units.
    * `slippage`: the maximum slippage as a percent string. `"1"` is 1%.
    * `gasPrice`: a gas price hint in gwei, required for single-chain quotes. Send whole gwei as a string, rounded up: some sources reject fractional gwei. On a chain whose gas price is below 1 gwei, that rounds up to `"1"`, above the real price. The hint doesn't set your transaction's gas price.
  </Step>

  <Step title="Read the response">
    A successful quote looks like this. Yours has different numbers, and often different routes, because prices move.

    ```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
                    }
                  ]
                }
              ]
            }
          ],
          "gasMultiplier": "NONE",
          "integratorFeeBreakdown": {
            "protocolFeeBps": 15,
            "integratorMarginBps": 0,
            "protocolFeeAmount": "14969",
            "integratorMarginAmount": "0"
          }
        }
      },
      "meta": {
        "requestId": "EdttSh29oAMEPhw=",
        "version": "v1",
        "accountType": "integrator",
        "apiKeyId": "00000000-0000-4000-8000-000000000000"
      }
    }
    ```

    | Field | Meaning |
    | - | - |
    | `quote.outAmount` | Expected output as an integer string in the output token's **base units**. USDC has 6 decimals, so `"9979975"` is 9.979975 USDC. |
    | `quote.aggregatorId` | The source that produced the best route. Pass it to `POST /swap` to build the transaction for this route. |
    | `quote.aggregatorOrder` | Every source that returned a quote, best first. If `/swap` fails with the first source, or its calldata reverts in `eth_estimateGas`, try the next. |
    | `quote.estimatedGas` | Gas units estimated by the source for its part of the route. It doesn't include the Olympex contracts, so estimate the transaction's gas yourself. `"0"` means the source gave no estimate. |
    | `quote.routes` | The routes the source reports, with the DEXes used for each hop. `percentage` is what the source reports for each route: later hops can appear as separate routes at 100, so the values don't always sum to 100. Use it for display only. In `subRoutes`, `from` and `to` are token addresses as the source reports them; case and the native-token address vary by source. |
    | `quote.integratorFeeBreakdown` | The Olympex protocol fee and your integrator fee, present on every quote. `protocolFeeBps` is in basis points: `15` is 0.15%. The protocol fee applies even without `fees` in the request, and `integratorMarginBps` is then `0`. See [Gas and fees](/concepts/gas-and-fees#the-fee-breakdown). |
    | `meta.requestId` | Identifies this request. Include it when you contact support. |

    To display `outAmount`, divide it by 10 to the power of the output token's decimals: the `decimals` that `GET /tokens` returns for the token, or the token contract's `decimals()`. Libraries such as viem and ethers do this with `formatUnits`.

    <Note>
      A quote is not reserved. Prices move between the quote and the swap, so when you build a swap, request it right after the quote and protect it with `slippage`.
    </Note>
  </Step>
</Steps>

## Verify

You're set up when both calls return `"success": true`:

* The chain list includes `137`.
* The quote returns an `outAmount`, an `aggregatorId` and a `meta.requestId`.

If a call fails, match the response:

| Response | Cause | Fix |
| - | - | - |
| `401` `{"message": "Unauthorized"}` | A signing header is missing, usually because the request was sent without the signer. | Send the request through `olympex_request` or `olympexRequest`. |
| `403` `{"message": "Forbidden"}` | The request was rejected: a wrong API key ID, secret key or passphrase, a clock more than 300 seconds off, a reused nonce, headers signed for another method, path or query, or an inactive account. | Send the request once more; the signer creates a new nonce for every call. If it fails again, stop: export the three values again, exactly as the console showed them, unset `OLYMPEX_NONCE` and `OLYMPEX_TIMESTAMP` if you set them, and sync your system clock. If correctly signed requests keep failing, contact [partners@olympex.io](mailto:partners@olympex.io) with the UTC time and the `apigw-requestid` response header of a failing request. |
| `403` `FORBIDDEN` with `Invalid body hash` | The body sent doesn't match the signed hash. With the shell function, the body wasn't canonical. | Don't retry it unchanged. Sort keys at every level and remove all whitespace, or use the TypeScript or Python signer. |
| `400` `VALIDATION_ERROR` | A field is missing or has the wrong type. | Fix the fields listed in `error.details`. For example, `chainId` must be a JSON integer (`137`), not a string. |
| `404` `NOT_FOUND` (`"No route for POST /api/v1/chains"`) | The path or the method is wrong, for example `POST /chains`. | Send `GET /chains` and `POST /quotes`, with the paths exactly as shown. |
| `500` or `503` `{"message": …}` | The gateway got no response from Olympex in time. A `500` can also answer the first request after a quiet period. | Send the request once more, then retry with backoff. The signer signs every attempt again with a new nonce. |

[Errors and retries](/authentication/errors-and-retries) lists every error code.

## Common pitfalls

<Warning>
  **`outAmount` is in base units.** `"9979975"` is 9.979975 USDC, not almost ten million. Shown without conversion, it overstates the output by a factor of 10 to the power of the token's decimals. The input `amount` is the opposite: a human-readable decimal.
</Warning>

<Warning>
  **Treat the passphrase like the secret key.** It is sent on every request, so anything that logs request headers captures it. Keep all three values out of source control, client-side code, tickets and chat. `export` lines can end up in your shell history.
</Warning>

<Warning>
  **There is no sandbox.** Your test key is a real account on the live API. Chain and token lists, quotes and chain checks are read-only, but the calldata that `POST /swap` returns is real mainnet calldata: broadcasting it moves funds. A limit order or DCA strategy you create with a test key is a real order too, and Olympex can execute it against the maker wallet's allowance.
</Warning>

## What's next

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

  <Card title="Place a limit order" icon="clock" href="/guides/create-a-limit-order">
    Sign the token pair, approve the order contract, create an order and follow it to completion.
  </Card>

  <Card title="Sign requests" icon="key" href="/authentication/sign-requests">
    The signing algorithm, requests without a body, the known-answer vectors and troubleshooting.
  </Card>

  <Card title="API reference overview" icon="code" href="/api-reference/overview">
    Every endpoint, with examples and an in-browser console.
  </Card>
</CardGroup>


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