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

# Execute a swap

> Build calldata with POST /swap, approve the exact amount, estimate gas and send a single-chain swap from the user's wallet.

This guide takes a single-chain quote to a confirmed swap: build the transaction with `POST /swap`, approve the exact input amount, estimate gas, send the transaction from the wallet and wait for the receipt. Olympex is non-custodial: it returns calldata and never signs or broadcasts the transaction.

<Info>
  You need API credentials and the signing helper from [Sign requests](/authentication/sign-requests), plus a wallet that holds the input token and native token for gas. If you haven't quoted yet, start with [Get a swap quote](/guides/get-a-swap-quote).
</Info>

## Prerequisites

* `OLYMPEX_API_KEY_ID`, `OLYMPEX_SECRET_KEY` and `OLYMPEX_PASSPHRASE` in your server's environment, and `sign-request.ts` saved next to your code.
* Node.js 22.18 or later, which runs `.ts` files directly, in an ES module project (`npm pkg set type=module`), and viem 2 (`npm install viem`). The same wallet steps with ethers v6 follow the steps.
* An RPC URL for the chain, as `POLYGON_RPC_URL` in this guide.
* A wallet funded on that chain with the input token (10 USDT here) and POL for gas. The script reads its private key from `WALLET_PRIVATE_KEY`.

## Steps

<Warning>
  `POST /swap` returns real mainnet calldata, and broadcasting it moves funds. Olympex has no testnet environment, so test with a small amount from a dedicated wallet.
</Warning>

<Steps>
  <Step title="Set up the clients and get a fresh quote">
    The snippets in these steps form one script, `execute-swap.ts`. Run it with `node execute-swap.ts`.

    <Note>
      In a dApp, split the work. Your server signs the Olympex requests and returns the `POST /swap` response to the browser; the user's wallet approves and sends the transaction, for example through `createWalletClient({ transport: custom(window.ethereum) })`. Your API credentials never reach the browser.
    </Note>

    ```ts theme={null}
    // execute-swap.ts. Run: node execute-swap.ts
    import {
      BaseError,
      ExecutionRevertedError,
      createPublicClient,
      createWalletClient,
      erc20Abi,
      http,
      isAddressEqual,
      parseUnits,
      type Address,
      type Hex,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { polygon } from "viem/chains";
    import { OlympexApiError, olympexRequest } from "./sign-request.ts"; // /authentication/sign-requests

    const NATIVE_TOKEN: Address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
    const GWEI = 1_000_000_000n;
    const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as Hex);
    const publicClient = createPublicClient({ chain: polygon, transport: http(process.env.POLYGON_RPC_URL) });
    const walletClient = createWalletClient({ account, chain: polygon, transport: http(process.env.POLYGON_RPC_URL) });

    const trade = {
      chainId: 137,
      inTokenAddress: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f" as Address, // USDT
      outTokenAddress: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" as Address, // USDC
      amount: "10", // human-readable units of the input token
      slippage: "1", // percent
      gasPrice: (((await publicClient.getGasPrice()) + GWEI - 1n) / GWEI).toString(), // hint in whole gwei, rounded up: some sources reject fractions ("1" below 1 gwei)
    };
    const isNative = isAddressEqual(trade.inTokenAddress, NATIVE_TOKEN);

    type QuoteData = { quote: { aggregatorId: string; aggregatorOrder: string[] | null } };

    const { quote } = await olympexRequest<QuoteData>("POST", "/quotes", {
      mode: "single-chain",
      params: trade,
    });
    ```

    Quote right before you build with [`POST /quotes`](/api-reference/quotes/get-quote): a quote is not reserved, and prices move. See [Slippage and price impact](/concepts/slippage-and-price-impact#quotes-are-not-reserved).
  </Step>

  <Step title="Build the transaction with POST /swap">
    Send the same pair, amount, slippage and gas price hint as the quote, plus two fields:

    * `account`: the wallet that sends the transaction and receives the output. The calldata is bound to it.
    * `aggregatorId`: the liquidity source to build with. `buildSwap` tries the quote's winner first and moves down `aggregatorOrder` when a source returns `422 NO_ROUTE` or `500 SWAP_ERROR`, or when its calldata reverts in the gas estimate (step 4).

    If you charge an integrator fee, send the same `fees` object you sent with the quote.

    ```ts theme={null}
    type SingleChainSwap = {
      to: Address;
      data: Hex;
      value: string; // wei
      contractToApprove: Address;
      outAmount: string; // base units of the output token
      minOutAmount: string; // base units; the transaction reverts below this
      gasLimit: string; // estimatedGas × 2: a fallback only
    };

    // The winning source first, then the other sources that quoted, best first.
    const sources = [quote.aggregatorId, ...(quote.aggregatorOrder ?? []).filter((id) => id !== quote.aggregatorId)];
    const revertedSources = new Set<string>(); // sources whose calldata reverted in the gas estimate (step 4)
    let aggregatorId = sources[0]; // the source that built the current swap

    async function buildSwap(): Promise<SingleChainSwap> {
      let lastError: unknown = new Error("No source left to build this swap: request a new quote");
      for (const id of sources) {
        if (revertedSources.has(id)) continue;
        try {
          const { swap } = await olympexRequest<{ swap: SingleChainSwap }>("POST", "/swap", {
            mode: "single-chain",
            params: { ...trade, account: account.address, aggregatorId: id },
          });
          // ERC-20 input never sends native value. See /concepts/security-model#check-before-you-sign.
          if (!isNative && swap.value !== "0") throw new Error(`Unexpected value ${swap.value} from ${id}`);
          aggregatorId = id;
          return swap;
        } catch (error) {
          if (!(error instanceof OlympexApiError) || !["NO_ROUTE", "SWAP_ERROR"].includes(error.code)) throw error;
          console.warn(`${error.code} from ${id} (requestId ${error.requestId}); trying the next source`);
          lastError = error;
        }
      }
      throw lastError;
    }

    let swap = await buildSwap();
    console.log(`Expect ${swap.outAmount}, at least ${swap.minOutAmount} (base units of USDC)`);
    ```

    The same request from cURL or Python:

    <CodeGroup>
      ```bash cURL theme={null}
      # Build a single-chain swap: 10 USDT to USDC on Polygon. Returns calldata; nothing is broadcast.
      # Needs openssl, curl and OLYMPEX_API_KEY_ID, OLYMPEX_SECRET_KEY, OLYMPEX_PASSPHRASE.
      METHOD=POST
      ENDPOINT=/swap
      QUERY=''
      BODY='{"mode":"single-chain","params":{"account":"0x1E67cb01969D79B2B895179e4A07D24a839dBb52","aggregatorId":"oneInch","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"
      ```

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

      data = olympex_request("POST", "/swap", {
          "mode": "single-chain",
          "params": {
              "chainId": 137,
              "inTokenAddress": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",  # USDT
              "outTokenAddress": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",  # USDC
              "account": "0x1E67cb01969D79B2B895179e4A07D24a839dBb52",  # sends the transaction
              "amount": "10",
              "slippage": "1",
              "gasPrice": "35",
              "aggregatorId": "oneInch",  # from the quote
          },
      })
      swap = data["swap"]
      print(swap["to"], swap["contractToApprove"], swap["minOutAmount"])
      ```
    </CodeGroup>

    A response looks like this, with the calldata shortened:

    ```json Response theme={null}
    {
      "success": true,
      "data": {
        "mode": "single-chain",
        "swap": {
          "to": "0xA91e13e8BfEbbC57ea008bfb2dE94AdD3F484E68",
          "gasLimit": "3000000",
          "contractToApprove": "0xa754451D6d32aB624111e5120409a474D29C2364",
          "data": "0x8ad0a76c0000000000000000000000000000000000000000000000000000000000000020…0000000000000000",
          "minOutAmount": "9891061",
          "outAmount": "9991223",
          "value": "0",
          "estimatedGas": "1500000"
        }
      },
      "meta": {
        "requestId": "Edt_pjJloAMEVjg=",
        "version": "v1",
        "accountType": "integrator",
        "apiKeyId": "00000000-0000-4000-8000-000000000000"
      }
    }
    ```

    | Field | Use |
    | - | - |
    | `to` | The Olympex aggregator contract: the transaction's `to`. |
    | `data` | The transaction calldata. |
    | `value` | Native token to send with the transaction, in wei. `"0"` for ERC-20 input. |
    | `contractToApprove` | The ERC-20 spender to approve. It can differ from `to`, as in this example. |
    | `outAmount`, `minOutAmount` | Expected and minimum output in base units of the output token. The transaction reverts below `minOutAmount`. Show both to the user before they confirm. |
    | `gasLimit`, `estimatedGas` | `gasLimit` is `estimatedGas × 2`. `estimatedGas` is `"1500000"` when the source gives no estimate, as in this example, and can be `"0"`. Otherwise it is the source's estimate for its part of the route, without the Olympex contracts. Estimate gas yourself. |

    [Security model](/concepts/security-model#check-before-you-sign) lists every check to run before a wallet signs.
  </Step>

  <Step title="Approve the exact input amount">
    For ERC-20 input, the wallet must allow `contractToApprove` to spend the input amount. Approve exactly that amount, in base units of the input token, and never an unlimited amount. An approval takes at least a block, so build the swap again once it confirms: the calldata's expiry window then starts after the approval.

    ```ts theme={null}
    /** Approves exactly `amount` for `spender`. Returns true if it sent a transaction. */
    async function ensureAllowance(token: Address, spender: Address, amount: bigint): Promise<boolean> {
      const allowance = await publicClient.readContract({
        address: token,
        abi: erc20Abi,
        functionName: "allowance",
        args: [account.address, spender],
      });
      if (allowance >= amount) return false;

      const approve = async (value: bigint) => {
        const hash = await walletClient.writeContract({ address: token, abi: erc20Abi, functionName: "approve", args: [spender, value] });
        const receipt = await publicClient.waitForTransactionReceipt({ hash });
        if (receipt.status !== "success") throw new Error(`approve(${value}) reverted: ${hash}`);
      };
      // Some tokens (USDT on Ethereum, for example) revert when one non-zero allowance
      // replaces another, so reset it to 0 first.
      if (allowance > 0n) await approve(0n);
      await approve(amount);
      return true;
    }

    /** Checks the balance and approves the current swap's spender, building again after each approval. */
    async function approveInput(): Promise<void> {
      if (isNative) return; // native input: `value` carries the amount, nothing to approve
      const decimals = await publicClient.readContract({ address: trade.inTokenAddress, abi: erc20Abi, functionName: "decimals" });
      const amountIn = parseUnits(trade.amount, decimals); // "10" USDT is 10000000 base units
      const balance = await publicClient.readContract({ address: trade.inTokenAddress, abi: erc20Abi, functionName: "balanceOf", args: [account.address] });
      if (balance < amountIn) throw new Error(`The wallet holds ${balance} base units of the input token, less than ${amountIn}`);
      // Build again after each approval, and check the allowance against the spender in the new response.
      while (await ensureAllowance(trade.inTokenAddress, swap.contractToApprove, amountIn)) {
        swap = await buildSwap();
      }
    }

    await approveInput();
    ```

    `ensureAllowance` resets any non-zero allowance to 0 before it approves, which costs one extra transaction on tokens that don't need it. If you keep a list of tokens that require the reset, reset only for those. `approveInput` also checks the balance first, so a gas estimate that reverts in the next step points at the route itself.

    **Native-token input.** To sell the chain's native token, set `inTokenAddress` to `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. There is nothing to approve: the swap response sets `value`, in wei, and the transaction carries it. Keep extra native token in the wallet for gas. The `uniswapV3Hermes` source doesn't support native-token input.
  </Step>

  <Step title="Estimate gas, and fall back if the calldata reverts">
    Always run `eth_estimateGas` on the exact transaction before you send it, and add a buffer (20% here). A `200` from `POST /swap` doesn't guarantee that the calldata executes: a source can build calldata that reverts. Don't size the gas limit from the quote's `estimatedGas` either: it covers only the source's part of the route, not the Olympex contracts. Use `gasLimit` from the response only when your RPC can't produce an estimate, and never when it's `"0"`.

    ```ts theme={null}
    /** Estimates the current swap's gas with a 20% buffer, or returns undefined if its calldata reverts. */
    async function estimateSwapGas(): Promise<bigint | undefined> {
      try {
        const estimate = await publicClient.estimateGas({ account, to: swap.to, data: swap.data, value: BigInt(swap.value) });
        return (estimate * 120n) / 100n; // +20% buffer
      } catch (error) {
        if (error instanceof BaseError && error.walk((e) => e instanceof ExecutionRevertedError)) return undefined; // would revert
        if (swap.gasLimit === "0") throw error; // no usable fallback: don't send it
        return BigInt(swap.gasLimit); // the RPC could not estimate: fall back to the API's value
      }
    }

    let gas = await estimateSwapGas();
    while (gas === undefined) {
      // The balance and the allowance are in place, so this route can't execute: build with the next source.
      console.warn(`Calldata from ${aggregatorId} reverts in eth_estimateGas; trying the next source`);
      revertedSources.add(aggregatorId);
      swap = await buildSwap(); // throws when no source is left: request a new quote then
      await approveInput(); // another source can use another spender
      gas = await estimateSwapGas();
    }
    const request = { account, to: swap.to, data: swap.data, value: BigInt(swap.value) };
    console.log(`Sending ${aggregatorId} calldata: expect ${swap.outAmount}, at least ${swap.minOutAmount}`);
    ```

    An estimate that reverts means the transaction would revert, so never send it. `approveInput` has checked the balance and the allowance, and the calldata is seconds old, so the route as built can't execute: the source built calldata that reverts, a market maker's quote inside the route has already expired, or the price moved past your slippage. The loop builds the swap with the next source in `aggregatorOrder`, approves its spender if it differs, and estimates again. When no source is left, `buildSwap` throws: request a new quote and start again. A fallback source can return less, so show the user the new `outAmount` and `minOutAmount` before they sign. [Gas and fees](/concepts/gas-and-fees#estimatedgas-and-gaslimit-are-estimates) explains why `gasLimit` is only a fallback.
  </Step>

  <Step title="Send the transaction right away">
    ```ts theme={null}
    const hash = await walletClient.sendTransaction({ ...request, gas });
    console.log(`Sent ${hash}`);
    ```

    Send as soon as the estimate succeeds. The calldata carries an on-chain expiry (5 minutes on most routes), and some routes include a market maker's quote that expires within seconds of the build: if the estimate reverts because that quote expired, build the swap again, as step 4 does. The calldata is also bound to `account`, and another Olympex swap from the same account can invalidate it. Don't queue or cache calldata; build it for each transaction.
  </Step>

  <Step title="Wait for the receipt">
    ```ts theme={null}
    const receipt = await publicClient.waitForTransactionReceipt({ hash });
    if (receipt.status !== "success") {
      throw new Error(`Swap reverted in block ${receipt.blockNumber}: ${hash}`);
    }
    console.log(`Confirmed in block ${receipt.blockNumber}`);
    ```

    A single-chain swap is final when its transaction is. The receipt from your RPC is the source of truth. To ask Olympex instead, poll [`GET /transactions/{hash}`](/api-reference/transactions/get-transaction): it reports the same result, `success` or `reverted`. [Track a swap to finality](/guides/track-a-swap-to-finality) covers confirmations, timeouts and replaced transactions.
  </Step>
</Steps>

<Accordion title="Wallet steps with ethers v6">
  This replaces steps 3 to 6 for a wallet built with ethers v6 (`npm install ethers`). It reuses `trade`, `isNative`, `swap`, `aggregatorId`, `revertedSources` and `buildSwap` from steps 1 and 2. Unlike the viem version, it doesn't fall back to `gasLimit` when the node returns an error: ethers reports any node error on `eth_estimateGas` as `CALL_EXCEPTION`, so it treats every such error as a revert and moves to the next source, and the `gasLimit` fallback runs only on network or HTTP errors.

  ```ts theme={null}
  import { Contract, JsonRpcProvider, Wallet, isError, parseUnits as parseUnitsEthers } from "ethers";

  const provider = new JsonRpcProvider(process.env.POLYGON_RPC_URL);
  const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!, provider);
  const ERC20_ABI = [
    "function allowance(address owner, address spender) view returns (uint256)",
    "function approve(address spender, uint256 amount) returns (bool)",
    "function balanceOf(address account) view returns (uint256)",
    "function decimals() view returns (uint8)",
  ];

  /** Approves exactly `amount` for `spender`. Returns true if it sent a transaction. */
  async function ensureAllowanceEthers(token: string, spender: string, amount: bigint): Promise<boolean> {
    const erc20 = new Contract(token, ERC20_ABI, wallet);
    const allowance: bigint = await erc20.allowance(wallet.address, spender);
    if (allowance >= amount) return false;
    // Some tokens (USDT on Ethereum, for example) revert when one non-zero allowance
    // replaces another, so reset it to 0 first.
    if (allowance > 0n) await (await erc20.approve(spender, 0n)).wait();
    await (await erc20.approve(spender, amount)).wait(); // wait() throws if the approval reverts
    return true;
  }

  /** Checks the balance and approves the current swap's spender, building again after each approval. */
  async function approveInputEthers(): Promise<void> {
    if (isNative) return;
    const token = new Contract(trade.inTokenAddress, ERC20_ABI, provider);
    const amountIn = parseUnitsEthers(trade.amount, (await token.decimals()) as bigint);
    const balance: bigint = await token.balanceOf(wallet.address);
    if (balance < amountIn) throw new Error(`The wallet holds ${balance} base units of the input token, less than ${amountIn}`);
    while (await ensureAllowanceEthers(trade.inTokenAddress, swap.contractToApprove, amountIn)) {
      swap = await buildSwap(); // fresh calldata after the approval
    }
  }

  /** Estimates the current swap's gas with a 20% buffer, or returns undefined if eth_estimateGas fails. */
  async function estimateSwapGasEthers(): Promise<bigint | undefined> {
    try {
      return ((await wallet.estimateGas({ to: swap.to, data: swap.data, value: BigInt(swap.value) })) * 120n) / 100n;
    } catch (error) {
      // ethers reports every JSON-RPC error from eth_estimateGas as CALL_EXCEPTION, reverted or not: don't send.
      if (isError(error, "CALL_EXCEPTION")) return undefined;
      if (swap.gasLimit === "0") throw error;
      return BigInt(swap.gasLimit); // other failures, such as network or HTTP errors: fall back to the API's value
    }
  }

  await approveInputEthers();
  let gasLimit = await estimateSwapGasEthers();
  while (gasLimit === undefined) {
    console.warn(`Calldata from ${aggregatorId} fails eth_estimateGas; trying the next source`);
    revertedSources.add(aggregatorId);
    swap = await buildSwap(); // throws when no source is left: request a new quote then
    await approveInputEthers();
    gasLimit = await estimateSwapGasEthers();
  }

  const tx = { to: swap.to, data: swap.data, value: BigInt(swap.value) };
  const sent = await wallet.sendTransaction({ ...tx, gasLimit });
  console.log(`Sent ${sent.hash}`);
  const receipt = await sent.wait(); // throws CALL_EXCEPTION if the swap reverts
  console.log(`Confirmed in block ${receipt?.blockNumber}`);
  ```
</Accordion>

## Verify

* `receipt.status` is `"success"`, and the transaction appears on the chain's block explorer (Polygonscan for Polygon) with `to` set to the `to` from `POST /swap`.
* The wallet received at least `minOutAmount` of the output token. For ERC-20 output, add up the token's `Transfer` events to `account` in the receipt:

```ts theme={null}
import { formatUnits, parseEventLogs } from "viem";

const received = parseEventLogs({ abi: erc20Abi, eventName: "Transfer", logs: receipt.logs })
  .filter((log) => isAddressEqual(log.address, trade.outTokenAddress) && isAddressEqual(log.args.to, account.address))
  .reduce((sum, log) => sum + log.args.value, 0n);

console.log(`Received ${formatUnits(received, 6)} USDC`); // USDC on Polygon has 6 decimals
if (received < BigInt(swap.minOutAmount)) {
  console.warn("Received less than minOutAmount: check the output token and account.");
}
```

For native-token output there is no `Transfer` event; compare the wallet's native balance before and after instead, allowing for the gas you paid.

## Common pitfalls

<Warning>
  **Expired or superseded calldata.** Calldata carries an on-chain expiry (5 minutes on most routes), and a market maker's quote inside some routes expires within seconds of the build. It is also bound to `account`, and another Olympex swap from the same account can invalidate earlier calldata. Build each swap right before you send it, and send one swap at a time per account.
</Warning>

<Warning>
  **Sending calldata without an estimate.** A `200` from `POST /swap` doesn't mean the calldata executes. Run `eth_estimateGas` on every transaction before the wallet signs it, and if it reverts, build the swap with the next source in `aggregatorOrder` instead of sending it.
</Warning>

<Warning>
  **Approving the wrong spender or an unlimited amount.** Approve `contractToApprove`, not `to`, and approve the exact input amount. An unlimited approval leaves the wallet exposed long after the swap.
</Warning>

<Warning>
  **Trusting `gasLimit`.** `gasLimit` is `estimatedGas × 2`, and `estimatedGas` can be a placeholder (`"1500000"`) or `"0"`. Estimate gas yourself and use `gasLimit` only when your RPC can't estimate, and never when it's `"0"`.
</Warning>

<Warning>
  **Sending from a different wallet.** Send the transaction from the `account` you passed to `POST /swap`. The calldata is built for that address, and the output goes to it.
</Warning>

<Warning>
  **Retrying a broadcast blindly.** If `sendTransaction` throws, the transaction may still have reached the chain. Before you build and send a new swap, look up the first hash or compare the account's nonce, or the user can end up swapping twice.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="Track a swap to finality" icon="clock" href="/guides/track-a-swap-to-finality">
    Confirmations, timeouts and replaced transactions.
  </Card>

  <Card title="Cross-chain swap end-to-end" icon="link" href="/guides/cross-chain-swap-end-to-end">
    The same flow across two chains.
  </Card>

  <Card title="Build a swap" icon="code" href="/api-reference/swap/build-swap">
    Every field of `POST /swap`.
  </Card>

  <Card title="Security model" icon="shield-halved" href="/concepts/security-model">
    Approvals, calldata binding and the checks to run before signing.
  </Card>
</CardGroup>


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