Skill: aaronllee/dex-quote (version 1.0.0)

--- name: okx-dex-quote description: > Fetch optimal swap quotes from OKX DEX Aggregator API (v6). Use this skill when a user wants to: 1. Get the best price for swapping tokens on any supported EVM/Solana chain 2. Compare DEX routing paths and price impact for token swaps 3. Build applications or scripts that query real-time DEX aggregator pricing Core value: Generates correct, authenticated API calls with proper token decimals, amount formatting, HMAC-SHA256 signing, and comprehensive error handling. version: 1.0.0 allowed_tools: [bash_tool, create_file, str_replace, view] required_context: [api_key, secret_key, passphrase] license: MIT author: Claude Assistant tags: [defi, dex, okx, swap, quote, web3, aggregator, evm, solana] --- # OKX DEX Aggregator Quote Skill ## Overview This skill generates production-ready code for fetching optimal swap quotes from the **OKX DEX Aggregator API v6**. The API finds the best price across multiple DEX protocols (Uniswap, SushiSwap, Curve, etc.) and returns detailed routing information including price impact, gas estimates, and token safety flags. **Key capabilities:** - Authenticated API requests with HMAC-SHA256 signing - Correct handling of token decimals and amount formatting - Support for `exactIn` (fixed input) and `exactOut` (fixed output) swap modes - Multi-chain support (Ethereum, BSC, Arbitrum, Base, Solana, and 20+ chains) - Price impact protection and honeypot detection - Fee/commission splitting configuration ## Prerequisites ### Required Credentials Users must have an OKX Web3 API key set. The skill needs three values: - `OKX_ACCESS_KEY` — API key - `OKX_SECRET_KEY` — Secret key for HMAC signing - `OKX_PASSPHRASE` — Account passphrase ### Environment - **Python**: `requests`, `hmac`, `hashlib`, `base64`, `datetime` (all stdlib except `requests`) - **Node.js**: `axios` or `node-fetch`, built-in `crypto` - No additional blockchain dependencies required (this is a read-only quote endpoint) ### API Endpoint ``` GET https://web3.okx.com/api/v6/dex/aggregator/quote ``` ## Workflow ### Step 1: Validate User Input Before constructing the API call, validate: 1. **Chain ID (`chainIndex`)** — Must be a supported chain. Common values: - `1` = Ethereum - `56` = BSC - `137` = Polygon - `42161` = Arbitrum - `8453` = Base - `130` = Unichain - `501` = Solana 2. **Token addresses** — Must be valid contract addresses or the native token placeholder: - Native tokens (ETH, BNB, etc.): [REDACTED_SECRET] - Contract tokens: Full 42-character hex address (EVM) or base58 (Solana) 3. **Amount** — MUST include token decimals. This is the #1 source of errors: - 1 ETH (18 decimals) → `"1000000000000000000"` - 1 USDT (6 decimals) → `"1000000"` - 1 WBTC (8 decimals) → `"100000000"` - Formula: `amount_raw = human_amount * (10 ** token_decimals)` 4. **Swap mode** — `exactIn` (default) or `exactOut` - `exactOut` only supported on: Ethereum, Base, BSC, Arbitrum - `exactOut` only works with Uniswap V2/V3 pools ### Step 2: Construct Authentication Headers OKX API requires HMAC-SHA256 signed requests with 4 headers: ``` OK-ACCESS-KEY: <api_key> OK-ACCESS-SIGN: <hmac_signature> OK-ACCESS-PASSPHRASE: <passphrase> OK-ACCESS-TIMESTAMP: <iso8601_timestamp> ``` **Signing algorithm:** ``` timestamp = ISO 8601 UTC time (e.g., "2025-01-15T12:00:00.000Z") prehash = timestamp + "GET" + request_path_with_query signature = Base64(HMAC-SHA256(secret_key, prehash)) ``` **CRITICAL**: The `request_path_with_query` must include the full path starting from `/api/...` plus the query string. Example: ``` /api/v6/dex/aggregator/quote?chainIndex=1&amount=1000000000000000000&fromTokenAddress=0xeee...&toTokenAddress=0xa0b... ``` ### Step 3: Send Request and Parse Response **Success response structure:** ```json { "code": "0", "data": [{ "chainIndex": "1", "fromToken": { "tokenSymbol": "ETH", "decimal": "18", ... }, "toToken": { "tokenSymbol": "USDC", "decimal": "6", ... }, "fromTokenAmount": "1000000000000000000", "toTokenAmount": "3521432100", "dexRouterList": [...], "tradeFee": "2.45", "estimateGasFee": "150000", "priceImpactPercent": "-0.12" }], "msg": "" } ``` **Key fields to extract:** - `toTokenAmount` — The amount you'll receive (in raw units with decimals) - `tradeFee` — Estimated network fee in USD - `priceImpactPercent` — Negative = losing value; large negative = danger - `dexRouterList` — Routing path through various DEX protocols - `fromToken.isHoneyPot` / `toToken.isHoneyPot` — Scam token flag - `fromToken.taxRate` / `toToken.taxRate` — Token buy/sell tax ### Step 4: Format Output for User Convert raw amounts back to human-readable: ``` human_amount = raw_amount / (10 ** decimal) ``` Always display: 1. Exchange rate (e.g., "1 ETH = 3,521.43 USDC") 2. Price impact percentage with warning if > 3% 3. Estimated gas fee in USD 4. Routing summary (which DEXes are used) 5. Honeypot warnings if detected 6. Token tax rates if > 0 ## Best Practices ### Security - **NEVER hardcode API credentials** in generated code. Always use environment variables or config files. - **NEVER log the secret key or signature** in debug output. - Validate all token addresses against expected format before sending. - Check `isHoneyPot` flag on both tokens and warn the user prominently. ### Amount Handling - **Always use string type** for amounts to avoid floating-point precision loss. - Python: Use `int()` for amount calculations, never `float()`. - JavaScript: Use `BigInt` or string math for amounts > 2^53. - When user provides a human-readable amount like "1.5 ETH", convert: `str(int(1.5 * 10**18))`. ### Error Handling - Check `response["code"] == "0"` before accessing data. - Common error codes: - Non-zero code with `msg` = API error (e.g., insufficient liquidity, invalid params) - HTTP 401 = Authentication failure (check signature algorithm) - HTTP 429 = Rate limited - If `priceImpactPercent` is `null`, warn that price impact couldn't be calculated. ### Rate Limits - Respect OKX rate limits. For production use, implement exponential backoff. - Cache quotes if polling repeatedly (quotes are valid for ~15-30 seconds). ### Optional Parameters Usage - `dexIds` — Use when you want to restrict to specific DEXes (e.g., only Uniswap). - `directRoute=true` — Only for Solana; forces single-pool routing. - `priceImpactProtectionPercent` — Set to a sensible default like `10` for safety. Set to `100` to disable. - `feePercent` — For integrators taking commission. Max 3% (EVM) or 10% (Solana). Up to 9 decimal places. ## Examples ### Example 1: Python — Get ETH to USDC Quote on Ethereum ```python import os, hmac, hashlib, base64, requests from datetime import datetime, timezone from urllib.parse import urlencode [REDACTED_SECRET]"OKX_ACCESS_KEY"] SECRET_KEY = os.environ["OKX_SECRET_KEY"] PASSPHRASE = os.environ["OKX_PASSPHRASE"] def get_okx_quote(chain_index, from_token, to_token, amount, swap_mode="exactIn"): base_url = "https://web3.okx.com" path = "/api/v6/dex/aggregator/quote" params = { "chainIndex": chain_index, "fromTokenAddress": from_token, "toTokenAddress": to_token, "amount": amount, "swapMode": swap_mode, } query_string = urlencode(params) request_path = f"{path}?{query_string}" # Generate timestamp and signature timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" prehash = timestamp + "GET" + request_path signature = base64.b64encode( hmac.new(SECRET_KEY.encode(), prehash.encode(), hashlib.sha256).digest() ).decode() headers = { "OK-ACCESS-KEY": API_KEY, "OK-ACCESS-SIGN": signature, "OK-ACCESS-PASSPHRASE": PASSPHRASE, "OK-ACCESS-TIMESTAMP": timestamp, } resp = requests.get(base_url + request_path, headers=headers) resp.raise_for_status() data = resp.json() if data["code"] != "0": raise Exception(f"OKX API error: {data['msg']}") return data["data"][0] # Swap 1 ETH -> USDC on Ethereum ETH = [REDACTED_SECRET] USDC = [REDACTED_SECRET] amount = str(1 * 10**18) # 1 ETH in wei quote = get_okx_quote("1", ETH, USDC, amount) to_decimals = int(quote["toToken"]["decimal"]) received = int(quote["toTokenAmount"]) / (10 ** to_decimals) print(f"You will receive: {received:,.2f} {quote['toToken']['tokenSymbol']}") print(f"Price impact: {quote['priceImpactPercent']}%") print(f"Gas fee (USD): ${quote['tradeFee']}") print(f"Honeypot check: {'WARNING' if quote['toToken']['isHoneyPot'] else 'Safe'}") ``` ### Example 2: Node.js — Get Quote with Commission Fee ```javascript const crypto = require("crypto"); const https = require("https"); const [REDACTED_SECRET]; const SECRET_KEY = process.env.OKX_SECRET_KEY; const PASSPHRASE = process.env.OKX_PASSPHRASE; function getQuote(chainIndex, fromToken, toToken, amount, options = {}) { const params = new URLSearchParams({ chainIndex, fromTokenAddress: fromToken, toTokenAddress: toToken, amount, swapMode: options.swapMode || "exactIn", ...(options.feePercent && { feePercent: options.feePercent }), ...(options.priceImpactProtectionPercent && { priceImpactProtectionPercent: options.priceImpactProtectionPercent, }), }); const path = `/api/v6/dex/aggregator/quote?${params}`; const timestamp = new Date().toISOString(); const prehash = timestamp + "GET" + path; const signature = crypto .createHmac("sha256", SECRET_KEY) .update(prehash) .digest("base64"); return new Promise((resolve, reject) => { const req = https.get( `https://web3.okx.com${path}`, { headers: { "OK-ACCESS-KEY": API_KEY, "OK-ACCESS-SIGN": signature, "OK-ACCESS-PASSPHRASE": PASSPHRASE, "OK-ACCESS-TIMESTAMP": timestamp, }, }, (res) => { let body = ""; res.on("data", (chunk) => (body += chunk)); res.on("end", () => { const data = JSON.parse(body); if (data.code !== "0") reject(new Error(`OKX: ${data.msg}`)); else resolve(data.data[0]); }); } ); req.on("error", reject); }); } ``` ### Example 3: Understanding the Routing Path ``` Router: ETH -> WBTC -> USDC dexRouterList shows: - 55% ETH -> WBTC via Uniswap V4 - 30% ETH -> USDC via Uniswap V4 (direct) - WBTC -> USDC via Euler (73%) + Uniswap V4 (27%) This means the aggregator splits the trade across multiple paths to minimize price impact and maximize output. ``` ## Troubleshooting | Problem | Cause | Solution | |---------|-------|----------| | `401 Unauthorized` | Signature mismatch | Verify prehash string is exactly `timestamp + "GET" + full_path_with_query`. Check secret key encoding. | | Amount too small | Missing decimal conversion | Multiply human amount by `10 ** token_decimals`. 1 USDT = `1000000`, not `1`. | | Invalid token address | Wrong chain/address combo | Verify the token exists on the specified chain. Use native token placeholder for ETH/BNB/etc. | | `priceImpactPercent` very negative (< -10%) | Low liquidity or large trade | Reduce trade size or split across multiple transactions. | | `isHoneyPot: true` | Scam token detected | **Do NOT proceed with the swap.** Warn the user immediately. | | `exactOut` not working | Unsupported chain/protocol | Only works on Ethereum, Base, BSC, Arbitrum with Uniswap V2/V3. | | `taxRate > 0` | Token has built-in tax | Factor tax into expected output. E.g., 5% tax means receiving 5% less. | | Empty `dexRouterList` | No liquidity path found | Try a different token pair, smaller amount, or different chain. | | `code` is non-zero | General API error | Read `msg` field for details. Common: rate limit, maintenance, invalid params. | ## Reference: Common Chain IDs | Chain | chainIndex | |-------|-----------| | Ethereum | 1 | | BSC | 56 | | Polygon | 137 | | Arbitrum | 42161 | | Optimism | 10 | | Avalanche | 43114 | | Base | 8453 | | Solana | 501 | | Unichain | 130 | ## Reference: Common Token Addresses (Ethereum) | Token | Address | Decimals | |-------|---------|----------| | ETH (native) | [REDACTED_SECRET] | 18 | | USDT | [REDACTED_SECRET] | 6 | | USDC | [REDACTED_SECRET] | 6 | | WETH | [REDACTED_SECRET] | 18 | | WBTC | [REDACTED_SECRET] | 8 | | DAI | [REDACTED_SECRET] | 18 | ## Reference: Request Parameters Summary | Parameter | Required | Description | |-----------|----------|-------------| | `chainIndex` | Yes | Chain ID (e.g., `1` for Ethereum) | | `amount` | Yes | Amount in raw units (with decimals) | | `swapMode` | Yes | `exactIn` or `exactOut` | | `fromTokenAddress` | Yes | Sell token contract address | | `toTokenAddress` | Yes | Buy token contract address | | `dexIds` | No | Comma-separated DEX IDs to restrict routing | | `directRoute` | No | `true` for single-pool only (Solana only) | | `priceImpactProtectionPercent` | No | Max allowed price impact 0-100 (default 90) | | `feePercent` | No | Commission fee percentage for integrators |

path: scripts/dex_quote.py
content: """
OKX DEX Aggregator Quote Client
================================
A production-ready Python client for the OKX DEX Aggregator Quote API (v6).

Usage:
    from okx_dex_quote import OKXDexQuoteClient

    client = OKXDexQuoteClient(
        [REDACTED_SECRET]"OKX_ACCESS_KEY"],
        secret_key=os.environ["OKX_SECRET_KEY"],
        passphrase=os.environ["OKX_PASSPHRASE"],
    )

    result = client.get_quote(
        chain_index="1",
        from_token="0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
        to_token="0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
        human_amount=1.0,
        from_decimals=18,
    )
    print(result.summary())

Environment variables:
    OKX_ACCESS_KEY    - API key
    OKX_SECRET_KEY    - Secret key for HMAC signing
    OKX_PASSPHRASE    - Account passphrase
"""

import os
import hmac
import hashlib
import base64
import json
import re
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
from urllib.parse import urlencode

try:
    import requests
except ImportError:
    raise ImportError("Please install requests: pip install requests")


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

BASE_URL = "https://web3.okx.com"
QUOTE_PATH = "/api/v6/dex/aggregator/quote"

NATIVE_TOKEN = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"

CHAIN_NAMES = {
    "1": "Ethereum",
    "56": "BSC",
    "137": "Polygon",
    "42161": "Arbitrum",
    "10": "Optimism",
    "43114": "Avalanche",
    "8453": "Base",
    "501": "Solana",
    "130": "Unichain",
}

# Chains that support exactOut mode
EXACT_OUT_CHAINS = {"1", "8453", "56", "42161"}


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class TokenInfo:
    symbol: str
    address: str
    decimal: int
    unit_price: Optional[str]
    is_honeypot: bool
    tax_rate: float

    @classmethod
    def from_api(cls, data: dict) -> "TokenInfo":
        return cls(
            symbol=data.get("tokenSymbol", "UNKNOWN"),
            address=data.get("tokenContractAddress", ""),
            decimal=int(data.get("decimal", "18")),
            unit_price=data.get("tokenUnitPrice"),
            is_honeypot=data.get("isHoneyPot", False),
            tax_rate=float(data.get("taxRate", "0")),
        )


@dataclass
class DexRoute:
    dex_name: str
    percent: str
    from_symbol: str
    to_symbol: str

    @classmethod
    def from_api(cls, data: dict) -> "DexRoute":
        return cls(
            dex_name=data.get("dexProtocol", {}).get("dexName", "Unknown"),
            percent=data.get("dexProtocol", {}).get("percent", "0"),
            from_symbol=data.get("fromToken", {}).get("tokenSymbol", "?"),
            to_symbol=data.get("toToken", {}).get("tokenSymbol", "?"),
        )


@dataclass
class QuoteResult:
    chain_index: str
    from_token: TokenInfo
    to_token: TokenInfo
    from_amount_raw: str
    to_amount_raw: str
    trade_fee_usd: str
    estimate_gas: str
    price_impact_percent: Optional[str]
    routes: list[DexRoute] = field(default_factory=list)
    swap_mode: str = "exactIn"
    raw_response: dict = field(default_factory=dict, repr=False)

    @property
    def from_amount_human(self) -> float:
        return int(self.from_amount_raw) / (10 ** self.from_token.decimal)

    @property
    def to_amount_human(self) -> float:
        return int(self.to_amount_raw) / (10 ** self.to_token.decimal)

    @property
    def exchange_rate(self) -> float:
        if self.from_amount_human == 0:
            return 0.0
        return self.to_amount_human / self.from_amount_human

    @property
    def has_honeypot_risk(self) -> bool:
        return self.from_token.is_honeypot or self.to_token.is_honeypot

    @property
    def has_tax(self) -> bool:
        return self.from_token.tax_rate > 0 or self.to_token.tax_rate > 0

    def summary(self) -> str:
        lines = []
        chain_name = CHAIN_NAMES.get(self.chain_index, f"Chain {self.chain_index}")
        lines.append(f"=== OKX DEX Quote ({chain_name}) ===")
        lines.append(f"Swap: {self.from_amount_human:,.6f} {self.from_token.symbol} -> "
                      f"{self.to_amount_human:,.6f} {self.to_token.symbol}")
        lines.append(f"Rate: 1 {self.from_token.symbol} = {self.exchange_rate:,.6f} {self.to_token.symbol}")
        lines.append(f"Mode: {self.swap_mode}")

        if self.price_impact_percent:
            impact = float(self.price_impact_percent)
            warning = " ⚠️ HIGH IMPACT" if impact < -3 else ""
            lines.append(f"Price Impact: {self.price_impact_percent}%{warning}")
        else:
            lines.append("Price Impact: N/A (could not calculate)")

        lines.append(f"Gas Fee (USD): ${self.trade_fee_usd}")

        if self.has_honeypot_risk:
            lines.append("🚨 HONEYPOT WARNING: One or more tokens flagged as potential scam!")

        if self.has_tax:
            if self.from_token.tax_rate > 0:
                lines.append(f"Sell Tax ({self.from_token.symbol}): {self.from_token.tax_rate * 100:.2f}%")
            if self.to_token.tax_rate > 0:
                lines.append(f"Buy Tax ({self.to_token.symbol}): {self.to_token.tax_rate * 100:.2f}%")

        if self.routes:
            lines.append("Routing:")
            for r in self.routes:
                lines.append(f"  {r.percent}% {r.from_symbol} -> {r.to_symbol} via {r.dex_name}")

        return "\n".join(lines)


# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------

class OKXDexQuoteClient:
    """Client for the OKX DEX Aggregator Quote API."""

    def __init__(
        self,
        [REDACTED_SECRET] = None,
        secret_key: Optional[str] = None,
        passphrase: Optional[str] = None,
        timeout: int = 30,
    ):
        self.[REDACTED_SECRET] or os.environ.get("OKX_ACCESS_KEY", "")
        self.secret_key = secret_key or os.environ.get("OKX_SECRET_KEY", "")
        self.passphrase = passphrase or os.environ.get("OKX_PASSPHRASE", "")
        self.timeout = timeout
        self.session = requests.Session()

        if not all([self.api_key, self.secret_key, self.passphrase]):
            raise ValueError(
                "Missing credentials. Set OKX_ACCESS_KEY, OKX_SECRET_KEY, "
                "OKX_PASSPHRASE env vars or pass them to the constructor."
            )

    def _sign(self, timestamp: str, method: str, request_path: str) -> str:
        """Generate HMAC-SHA256 signature."""
        prehash = timestamp + method + request_path
        mac = hmac.new(
            self.secret_key.encode("utf-8"),
            prehash.encode("utf-8"),
            hashlib.sha256,
        )
        return base64.b64encode(mac.digest()).decode("utf-8")

    def _headers(self, request_path: str) -> dict:
        """Build authenticated headers."""
        timestamp = datetime.now(timezone.utc).strftime(
            "%Y-%m-%dT%H:%M:%S.%f"
        )[:-3] + "Z"
        signature = self._sign(timestamp, "GET", request_path)
        return {
            "OK-ACCESS-KEY": self.api_key,
            "OK-ACCESS-SIGN": signature,
            "OK-ACCESS-PASSPHRASE": self.passphrase,
            "OK-ACCESS-TIMESTAMP": timestamp,
        }

    @staticmethod
    def _validate_address(address: str, chain_index: str) -> None:
        """Basic address format validation."""
        if chain_index == "501":  # Solana uses base58
            if not re.match(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$", address):
                if address != NATIVE_TOKEN:
                    raise ValueError(f"Invalid Solana address: {address}")
        else:  # EVM chains use hex
            if not re.match(r"^0x[0-9a-fA-F]{40}$", address):
                raise ValueError(f"Invalid EVM address: {address}")

    @staticmethod
    def to_raw_amount(human_amount: float, decimals: int) -> str:
        """Convert human-readable amount to raw amount string.

        Uses integer math to avoid floating-point precision issues.
        """
        # Handle decimal amounts by splitting on '.'
        amount_str = f"{human_amount:.{decimals}f}"
        if "." in amount_str:
            integer_part, decimal_part = amount_str.split(".")
            decimal_part = decimal_part[:decimals].ljust(decimals, "0")
            raw = int(integer_part) * (10 ** decimals) + int(decimal_part)
        else:
            raw = int(amount_str) * (10 ** decimals)
        return str(raw)

    def get_quote(
        self,
        chain_index: str,
        from_token: str,
        to_token: str,
        human_amount: Optional[float] = None,
        raw_amount: Optional[str] = None,
        from_decimals: int = 18,
        swap_mode: str = "exactIn",
        dex_ids: Optional[str] = None,
        direct_route: Optional[bool] = None,
        price_impact_protection: Optional[str] = None,
        fee_percent: Optional[str] = None,
    ) -> QuoteResult:
        """Fetch a swap quote from OKX DEX Aggregator.

        Args:
            chain_index: Chain ID (e.g., "1" for Ethereum).
            from_token: Sell token contract address.
            to_token: Buy token contract address.
            human_amount: Amount in human-readable form (e.g., 1.5 for 1.5 ETH).
                         Requires from_decimals to be set correctly.
            raw_amount: Amount already in raw units (overrides human_amount).
            from_decimals: Decimal places for the from_token (default 18).
            swap_mode: "exactIn" or "exactOut".
            dex_ids: Comma-separated DEX IDs to restrict routing.
            direct_route: Force single-pool routing (Solana only).
            price_impact_protection: Max allowed price impact percent (0-100).
            fee_percent: Commission fee percentage for integrators.

        Returns:
            QuoteResult with parsed quote data.

        Raises:
            ValueError: Invalid parameters.
            Exception: API error.
        """
        # Validate inputs
        if swap_mode == "exactOut" and chain_index not in EXACT_OUT_CHAINS:
            raise ValueError(
                f"exactOut not supported on chain {chain_index}. "
                f"Supported: {', '.join(EXACT_OUT_CHAINS)}"
            )

        self._validate_address(from_token, chain_index)
        self._validate_address(to_token, chain_index)

        # Determine amount
        if raw_amount:
            amount = raw_amount
        elif human_amount is not None:
            amount = self.to_raw_amount(human_amount, from_decimals)
        else:
            raise ValueError("Provide either human_amount or raw_amount")

        # Build query parameters
        params = {
            "chainIndex": chain_index,
            "fromTokenAddress": from_token,
            "toTokenAddress": to_token,
            "amount": amount,
            "swapMode": swap_mode,
        }

        if dex_ids:
            params["dexIds"] = dex_ids
        if direct_route is not None:
            params["directRoute"] = str(direct_route).lower()
        if price_impact_protection:
            params["priceImpactProtectionPercent"] = price_impact_protection
        if fee_percent:
            params["feePercent"] = fee_percent

        query_string = urlencode(params)
        request_path = f"{QUOTE_PATH}?{query_string}"

        # Make request
        headers = self._headers(request_path)
        url = f"{BASE_URL}{request_path}"

        resp = self.session.get(url, headers=headers, timeout=self.timeout)
        resp.raise_for_status()
        data = resp.json()

        if data.get("code") != "0":
            raise Exception(f"OKX API error (code {data.get('code')}): {data.get('msg', 'Unknown error')}")

        if not data.get("data"):
            raise Exception("OKX API returned empty data — no quote available for this pair/amount.")

        quote_data = data["data"][0]

        # Parse response
        routes = [DexRoute.from_api(r) for r in quote_data.get("dexRouterList", [])]

        return QuoteResult(
            chain_index=quote_data.get("chainIndex", chain_index),
            from_token=TokenInfo.from_api(quote_data.get("fromToken", {})),
            to_token=TokenInfo.from_api(quote_data.get("toToken", {})),
            from_amount_raw=quote_data.get("fromTokenAmount", amount),
            to_amount_raw=quote_data.get("toTokenAmount", "0"),
            trade_fee_usd=quote_data.get("tradeFee", "0"),
            estimate_gas=quote_data.get("estimateGasFee", "0"),
            price_impact_percent=quote_data.get("priceImpactPercent"),
            routes=routes,
            swap_mode=quote_data.get("swapMode", swap_mode),
            raw_response=quote_data,
        )


# ---------------------------------------------------------------------------
# CLI usage
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="OKX DEX Quote CLI")
    parser.add_argument("--chain", default="1", help="Chain index (default: 1 = Ethereum)")
    parser.add_argument("--from", dest="from_token", required=True, help="From token address")
    parser.add_argument("--to", dest="to_token", required=True, help="To token address")
    parser.add_argument("--amount", type=float, required=True, help="Human-readable amount")
    parser.add_argument("--decimals", type=int, default=18, help="From token decimals (default: 18)")
    parser.add_argument("--mode", default="exactIn", choices=["exactIn", "exactOut"])

    args = parser.parse_args()

    client = OKXDexQuoteClient()
    result = client.get_quote(
        chain_index=args.chain,
        from_token=args.from_token,
        to_token=args.to_token,
        human_amount=args.amount,
        from_decimals=args.decimals,
        swap_mode=args.mode,
    )
    print(result.summary())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)