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

# Place a limit order

> Resolve the tokens, estimate the execution fee, approve the Olympex order contract, sign the token pair, create a limit order, follow it and cancel it.

A limit order sells `amount` of one token for another. The order executes when the market price of `inTokenAddress`, in `outTokenAddress`, reaches `priceTrigger` or better. Olympex executes it from the maker's wallet, so the wallet first approves the Olympex order contract and signs the token pair. This guide sells 0.5 WETH for USDC on Polygon at 4,200 USDC per WETH, follows the order, then cancels it and lowers the allowance.

<Info>
  You need API credentials and the signing helper from [Sign requests](/authentication/sign-requests), and a wallet on Polygon that holds WETH and POL for gas. [Order signatures and allowances](/concepts/order-authorization) explains what the wallet's signature and allowance let Olympex do: read it before you run this with real funds.
</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 ethers v6 (`npm install ethers`).
* An RPC URL for Polygon, as `POLYGON_RPC_URL`.
* A wallet that is an externally owned account (EOA), with its private key in `WALLET_PRIVATE_KEY`. It holds the WETH to sell plus a little more for the execution fee, and POL for its approval transactions. Smart-contract wallets (Safe, ERC-4337) can't sign orders: only 65-byte ECDSA signatures are accepted.

## Steps

<Warning>
  A limit order is a real trade. Olympex executes it from the wallet when the market reaches `priceTrigger` or better, and execution moves funds on mainnet. Use a small amount from a dedicated wallet and a price you're willing to trade at.
</Warning>

<Steps>
  <Step title="Set up and check the chain">
    The snippets in these steps form one script, `create-limit-order.ts`. Run it with `node create-limit-order.ts`.

    [`GET /chains`](/api-reference/chains/list-chains) returns the IDs of the chains Olympex has enabled, as integers. Check your chain there instead of hard-coding the list. Limit orders and DCA execute only on the chains in `ORDER_CONTRACTS`, where the Olympex order contract is deployed.

    ```ts theme={null}
    // create-limit-order.ts. Run: node create-limit-order.ts
    import { Contract, JsonRpcProvider, Wallet, formatUnits, getAddress, parseUnits } from "ethers";
    import { olympexRequest } from "./sign-request.ts"; // /authentication/sign-requests

    // The Olympex order contract: the spender that limit orders and DCA draw on. See /concepts/order-authorization.
    const ORDER_CONTRACTS: Record<number, string> = {
      1: "0xef636bca438D030Cead0AaBD74447Bdf85872F6B", // Ethereum
      10: "0xfc07731BE06D93afbfA66E2072B5188A35b93077", // Optimism
      56: "0xA95ccc6f0A561206dd1D0260C1ceE279CB4245A2", // BNB Chain
      137: "0x50186B03dc7315271FB58da0d3b9f2c65A51dA76", // Polygon
      8453: "0x0eE148d16beB83B08905Db4ad3c9c62d569153f0", // Base
      42161: "0x31B15abf6F2c924919605d7ccbc5FB41C8A56Bc1", // Arbitrum One
      43114: "0x1a362443D22a481867e1a20C16711f14102D3CB3", // Avalanche C-Chain
      59144: "0x0eE148d16beB83B08905Db4ad3c9c62d569153f0", // Linea
    };

    const CHAIN_ID = 137; // Polygon
    const WETH = "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"; // the token you sell
    const USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; // the token you buy
    const order = { amount: "0.5", priceTrigger: "4200", slippage: "1" }; // human-readable; USDC per 1 WETH; percent

    const provider = new JsonRpcProvider(process.env.POLYGON_RPC_URL);
    const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!, provider);
    const maker = getAddress(wallet.address); // EIP-55 checksummed

    const { chainIds } = await olympexRequest<{ chainIds: number[] }>("GET", "/chains");
    const orderContract = ORDER_CONTRACTS[CHAIN_ID];
    if (!chainIds.includes(CHAIN_ID) || !orderContract) throw new Error(`No limit orders on chain ${CHAIN_ID}`);
    ```

    `GET` requests have no body: the helper signs the empty string. On Polygon the order contract is [`0x50186B03dc7315271FB58da0d3b9f2c65A51dA76`](https://polygonscan.com/address/0x50186B03dc7315271FB58da0d3b9f2c65A51dA76). It's not the `contractToApprove` that `POST /swap` returns, which is for swaps.
  </Step>

  <Step title="Resolve the tokens with GET /tokens">
    [`GET /tokens`](/api-reference/tokens/list-tokens) returns every token Olympex lists on a chain, with its `address`, `symbol`, `name` and `decimals`. Match tokens by address, never by symbol, and compare addresses case-insensitively: their casing varies.

    ```ts theme={null}
    type Token = { address: string; symbol: string; name: string; decimals: number };

    const { tokens } = await olympexRequest<{ chainId: number; tokens: Token[] }>("GET", `/tokens?chainId=${CHAIN_ID}`);
    const findToken = (address: string): Token => {
      const token = tokens.find((t) => t.address.toLowerCase() === address.toLowerCase());
      // Unlisted tokens can be traded too: this example stops on them only to stay short.
      if (!token) throw new Error(`${address} isn't listed on chain ${CHAIN_ID}`);
      // Listed symbols can carry a suffix that tells duplicates apart ("STRK_1") or stray spaces: keep the plain symbol.
      return { ...token, symbol: token.symbol.trim().replace(/_\d+$/, "") };
    };
    const sell = findToken(WETH); // symbol "WETH", decimals 18
    const buy = findToken(USDC); // symbol "USDC", decimals 6
    ```

    The list holds hundreds of tokens and changes rarely, so cache it on your side, for example for 24 hours, instead of calling it for every order.

    Limit orders need each token's real symbol (step 6). A listed symbol can differ from the token contract's `symbol()`: a few carry a numeric suffix that tells tokens with the same symbol apart, such as `STRK_1`, and a few differ in case. `findToken` trims the symbol and drops a trailing `_<number>`. To be certain, read `symbol()` from the token contract instead.

    The token you sell must be an ERC-20. To sell a chain's native token, wrap it first: WETH ([`0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2`](https://etherscan.io/address/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)) on Ethereum, WBNB ([`0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c`](https://bscscan.com/address/0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c)) on BNB Chain or WPOL ([`0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270`](https://polygonscan.com/address/0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270)) on Polygon. ERC-20 tokens whose `transfer` and `approve` don't return a boolean can't be sold either: USDT isn't supported as the token you sell in limit orders or DCA on Ethereum. Neither are fee-on-transfer tokens.
  </Step>

  <Step title="Estimate the execution fee with POST /quotes">
    Olympex pays the gas to execute the order and takes it back from the maker in the token sold, as a second transfer. So the allowance you grant must cover `amount` plus that fee. Estimate the fee with [`POST /quotes`](/api-reference/quotes/get-quote) and `"includeGasInfo": true`: `dataFeeTransaction.transactionFeeInToken` is the fee in human-readable units of the token sold.

    ```ts theme={null}
    const GWEI = 1_000_000_000n;
    const gasPrice = BigInt(await provider.send("eth_gasPrice", [])); // wei
    const gasPriceGwei = ((gasPrice + GWEI - 1n) / GWEI).toString(); // whole gwei, rounded up ("1" below 1 gwei)

    type QuoteData = { quote: { dataFeeTransaction?: { transactionFeeInToken: string; valueToApprove: string } } };

    const { quote } = await olympexRequest<QuoteData>("POST", "/quotes", {
      mode: "single-chain",
      params: {
        chainId: CHAIN_ID,
        inTokenAddress: sell.address,
        outTokenAddress: buy.address,
        amount: order.amount,
        slippage: order.slippage,
        gasPrice: gasPriceGwei,
        includeGasInfo: true,
        gasMultiplier: "HIGH", // scales the gas estimate 4×: the buffer for this order's allowance
      },
    });
    const feeInToken = quote.dataFeeTransaction?.transactionFeeInToken;
    if (!feeInToken) throw new Error("The quote has no gas estimate: request it again");

    /** Converts a human-readable amount to base units, rounding up digits beyond the token's decimals. */
    const toBaseUnitsUp = (value: string | number, decimals: number): bigint => {
      const text = typeof value === "number" ? value.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 20 }) : value;
      const [whole, fraction = ""] = text.split(".");
      const units = parseUnits(`${whole}.${fraction.slice(0, decimals) || "0"}`, decimals);
      return /[1-9]/.test(fraction.slice(decimals)) ? units + 1n : units;
    };

    // What this order can draw: the amount plus the fee estimate, already scaled by gasMultiplier HIGH.
    const reserve = parseUnits(order.amount, sell.decimals) + toBaseUnitsUp(feeInToken, sell.decimals);
    console.log(`Reserve ${formatUnits(reserve, sell.decimals)} ${sell.symbol} for this order`);
    ```

    The fee is an estimate, and it runs low. It comes from the liquidity source's `estimatedGas`, which covers only the source's part of the route, not the Olympex contracts, and gas prices move between now and execution. So add a buffer: this guide requests the quote with `gasMultiplier` `HIGH`, which scales the gas estimate 4×, so `transactionFeeInToken` and `valueToApprove` (`amount` plus the fee) already include it. Size the buffer to how much gas prices move on your chain.

    `gasPriceGwei` is the chain's gas price in whole gwei, rounded up, the form quotes take, and the order stores it as its `gasPrice` in step 6. On a chain whose gas price is below 1 gwei, it rounds up to `"1"`, above the real price.
  </Step>

  <Step title="Approve the order contract">
    Read the wallet's current allowance for the order contract, then approve that value plus this order's reserve. The allowance is shared: every open limit order and active DCA strategy that sells the same token on the same chain draws on it. `approve` replaces the allowance instead of adding to it, so approving only this order's reserve would take it away from the others.

    ```ts theme={null}
    const ERC20_ABI = [
      "function allowance(address owner, address spender) view returns (uint256)",
      "function approve(address spender, uint256 value) returns (bool)",
      "function balanceOf(address account) view returns (uint256)",
    ];
    const sellToken = new Contract(sell.address, ERC20_ABI, wallet);
    const RESET_BEFORE_APPROVE = false; // true for tokens, such as USDT, that revert when one non-zero allowance replaces another

    /** Sets the order contract's allowance on the token sold to exactly `value`. */
    const setAllowance = async (value: bigint): Promise<void> => {
      const current: bigint = await sellToken.allowance(maker, orderContract);
      if (current === value) return;
      if (RESET_BEFORE_APPROVE && current > 0n && value > 0n) await (await sellToken.approve(orderContract, 0n)).wait();
      await (await sellToken.approve(orderContract, value)).wait(); // wait() throws if the approval reverts
    };

    const balance: bigint = await sellToken.balanceOf(maker);
    if (balance < reserve) throw new Error(`The wallet can't cover ${formatUnits(reserve, sell.decimals)} ${sell.symbol}`);

    const allowanceBefore: bigint = await sellToken.allowance(maker, orderContract);
    await setAllowance(allowanceBefore + reserve); // add to what other open orders need
    ```

    Nothing moves when you approve or create the order. At execution, Olympex pulls `amount` through the order contract, swaps it, sends all the output to the maker, and takes the fee. The wallet must still hold `amount` plus the fee then, or the order can't execute.

    The allowance is the on-chain limit on what Olympex can move from the wallet. Approve only what your open orders need, and never an unlimited amount.
  </Step>

  <Step title="Sign the token pair">
    The maker signs the pair once: `keccak256(abi.encodePacked(maker, maker, tokenIn, tokenOut))`, signed with `personal_sign` over its 32 raw bytes. Save this helper as `sign-order-pair.ts`:

    ```ts sign-order-pair.ts theme={null}
    import { getAddress, getBytes, solidityPackedKeccak256, verifyMessage, type Signer } from "ethers";
    export const signOrderPair = async (signer: Signer, tokenIn: string, tokenOut: string) => {
      const maker = getAddress(await signer.getAddress());
      const inner = solidityPackedKeccak256(["address", "address", "address", "address"], [maker, maker, getAddress(tokenIn), getAddress(tokenOut)]);
      const signature = await signer.signMessage(getBytes(inner)); // the 32 bytes, not the hex string
      if (verifyMessage(getBytes(inner), signature) !== maker) throw new Error("signature check failed");
      return { accountTo: maker, signature };
    };
    ```

    Then sign WETH for USDC:

    ```ts theme={null}
    import { signOrderPair } from "./sign-order-pair.ts";

    const { accountTo, signature } = await signOrderPair(wallet, sell.address, buy.address);
    ```

    Olympex doesn't check the signature when you create the order: a wrong one makes execution fail later. The self-check in `signOrderPair` catches the usual mistake, signing the hex string instead of the bytes. Address casing doesn't change the hash.

    The signature lets the Olympex order contract swap WETH for USDC from this wallet. It doesn't bind an amount, price, expiry, chain or order: Olympex enforces those when it executes. It stays valid after you cancel, and the same signature serves limit orders and DCA strategies that sell WETH for USDC. Selling USDC for WETH needs its own signature, because the token order is part of the hash.
  </Step>

  <Step title="Create the order with POST /limit-order">
    [`POST /limit-order`](/api-reference/limit-orders/create-limit-order) stores the order as `pending`. Olympex first checks that it has a reference price for the pair.

    | Field | What to send |
    | - | - |
    | `accountTo` | The maker, in its EIP-55 checksummed form. List filters match it case-sensitively. |
    | `inTokenAddress`, `outTokenAddress` | The token you sell (an ERC-20) and the token you buy. |
    | `tokenASymbol`, `tokenBSymbol` | The real symbols of the token you sell and the token you buy, for example `WETH` and `USDC`, without a list suffix such as `_1`. Olympex can use them to find the reference price and doesn't check them against the token contracts, so check the symbols the response returns. |
    | `amount` | Amount to sell, human-readable (`"0.5"` is 0.5 WETH), as a string. |
    | `priceTrigger` | Units of `outTokenAddress` per 1 `inTokenAddress`, human-readable, as a string. |
    | `price` | Optional. A copy of the limit price that Olympex stores as sent and never syncs with `priceTrigger`. Send the same value as `priceTrigger`, and send both whenever you change one. Responses return `0` when you omit it. |
    | `expired` | A Unix timestamp in milliseconds, as a 13-digit string, in the future and at most 365 days ahead. Olympex rejects any other value with `400 VALIDATION_ERROR`. Don't rely on it to stop the order: when you no longer want it, cancel it and lower the allowance. |
    | `gasPrice` | The chain's current gas price in gwei, rounded up, as a string. Olympex stores it with the order, but it isn't a cap: execution pays the gas price at that moment, and the maker pays back the execution transaction's actual gas cost, in the token sold. Size the allowance from the fee estimate, not from `gasPrice`. |
    | `slippage` | The maximum slippage in percent, as a string: `"1"` is 1%. |
    | `signature` | From the previous step. |

    Don't send any other field: the ones Olympex sets as it executes the order, such as `status` and `txHash`, return `400 VALIDATION_ERROR`.

    ```ts theme={null}
    type LimitOrder = {
      id: string;
      status: string; // pending, executing, submitted, completed, failed or cancelled
      inTokenAddress: string;
      outTokenAddress: string;
      tokenASymbol: string;
      tokenBSymbol: string;
      amount: number;
      priceTrigger: number;
      expired: string;
      txHash: string;
      reasonFail: string[];
      attemptNumber: number;
      deletedAt: string;
    };

    const expired = String(Date.now() + 7 * 24 * 60 * 60 * 1000); // one week from now

    const created = await olympexRequest<LimitOrder>("POST", "/limit-order", {
      accountTo,
      chainId: CHAIN_ID,
      inTokenAddress: getAddress(sell.address),
      outTokenAddress: getAddress(buy.address),
      tokenASymbol: sell.symbol,
      tokenBSymbol: buy.symbol,
      amount: order.amount,
      priceTrigger: order.priceTrigger,
      price: order.priceTrigger,
      expired,
      gasPrice: gasPriceGwei,
      slippage: order.slippage,
      signature,
    });
    console.log(`Order ${created.id} is ${created.status}`); // store the ID now

    // A real but wrong symbol is accepted, and the order is then priced from that token's market. Check both.
    const NORMALIZED: Record<string, string> = { WETH: "ETH", WBNB: "BNB", WPOL: "POL" };
    const sameSymbol = (returned: string, sent: string) =>
      [sent, NORMALIZED[sent.toUpperCase()]].some((s) => s?.toUpperCase() === returned.toUpperCase());
    if (!sameSymbol(created.tokenASymbol, sell.symbol) || !sameSymbol(created.tokenBSymbol, buy.symbol)) {
      await olympexRequest<LimitOrder>("DELETE", `/limit-order/${created.id}`);
      const current: bigint = await sellToken.allowance(maker, orderContract);
      await setAllowance(current > reserve ? current - reserve : 0n); // give back this order's reserve
      throw new Error(`Olympex priced the order as ${created.tokenASymbol}/${created.tokenBSymbol}: cancelled it`);
    }
    ```

    ```json Response theme={null}
    {
      "success": true,
      "data": {
        "id": "afc47108-d059-473c-b2e1-5f2ca7951466",
        "accountTo": "0x1E67cb01969D79B2B895179e4A07D24a839dBb52",
        "chainId": 137,
        "gasPrice": "35",
        "inTokenAddress": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
        "outTokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
        "slippage": "1",
        "tokenASymbol": "ETH",
        "tokenBSymbol": "USDC",
        "amount": 0.5,
        "price": 4200,
        "priceTrigger": 4200,
        "expired": "1791302400000",
        "txHash": "",
        "status": "pending",
        "reasonFail": [],
        "attemptNumber": 0,
        "allowance": "",
        "estimateGas": "",
        "effectivePriceGas": "",
        "createdAt": "2026-09-29T16:01:22.237Z",
        "updatedAt": "2026-09-29T16:01:22.237Z",
        "deletedAt": ""
      },
      "meta": {
        "requestId": "EeAQVh4coAMEJSw=",
        "version": "v1",
        "accountType": "integrator",
        "apiKeyId": "00000000-0000-4000-8000-000000000000"
      }
    }
    ```

    The response returns `chainId`, `amount`, `price` and `priceTrigger` as numbers. Symbols can come back normalized: `WETH` returns as `ETH`, `WBNB` as `BNB` and `WPOL` as `POL`. Any other returned symbol that isn't the one you sent means Olympex prices the order from another token's market: the check above cancels such an order. `deletedAt` is an empty string until the order is cancelled.

    If Olympex finds no reference price for the pair, the call fails with `400 VALIDATION_ERROR` "Not exist reference price for this pair WETH/USDC", with the symbols you sent, and no order is created. The error can be temporary, so retry later with backoff before you rule the pair out. See [Reference price](/concepts/limit-orders#reference-price).

    Each successful call creates a new order, and Olympex ignores any `id` you send. If the call times out or fails with a 5xx, don't send it again: the order may exist. List your orders first, and match on the `expired` value your script generated:

    ```ts theme={null}
    /** After a timeout or 5xx on the create, look for the order before you send it again. */
    const findCreatedOrder = async (): Promise<LimitOrder | undefined> =>
      (await olympexRequest<LimitOrder[]>("GET", `/limit-order?accountTo=${accountTo}&chainId=${CHAIN_ID}`))
        .find((o) => o.expired === expired && o.inTokenAddress === getAddress(sell.address));
    ```
  </Step>

  <Step title="Follow the order with GET /limit-order/{id}">
    Olympex doesn't notify you when an order changes: poll [`GET /limit-order/{id}`](/api-reference/limit-orders/get-limit-order).

    | `status` | Meaning | Final |
    | - | - | - |
    | `pending` | Waiting for the market to reach `priceTrigger`. You can update or cancel it. | No |
    | `executing`, `submitted` | Olympex is executing the order. | No |
    | `completed` | Executed. `txHash` is the execution transaction on the order's chain. | Yes |
    | `failed` | Execution failed. `reasonFail` says why, and `attemptNumber` counts the attempts. | Yes |
    | `cancelled` | Cancelled with `DELETE`. `deletedAt` is the time of the `DELETE` call. | Yes |

    Treat any other value as not final, and give your poller a cap: an order can stay in a status that isn't final.

    ```ts theme={null}
    const FINAL = new Set(["completed", "failed", "cancelled"]);
    const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

    /** Polls until the order is final or `stopAt` passes, and returns it either way. */
    const followOrder = async (id: string, stopAt: number, intervalMs = 30_000): Promise<LimitOrder> => {
      for (;;) {
        const current = await olympexRequest<LimitOrder>("GET", `/limit-order/${id}`);
        if (FINAL.has(current.status) || Date.now() >= stopAt) return current;
        await sleep(intervalMs);
      }
    };

    let final = await followOrder(created.id, Date.now() + 5 * 60_000); // this demo gives the order 5 minutes
    if (final.status === "completed") console.log(`Executed in ${final.txHash}`);
    else if (final.status === "failed") console.log(`Failed after ${final.attemptNumber} attempts: ${final.reasonFail.join("; ")}`);
    else if (final.status !== "pending") console.log(`Still ${final.status}: check it again later`);
    ```

    An order can stay `pending` until the market reaches its price, so poll at an interval that suits your product, and route the `GET` through your retry wrapper: `GET` is safe to repeat. [`GET /limit-order`](/api-reference/limit-orders/list-limit-orders) lists every order created with your API key, with the optional filters `status`, `chainId` and `accountTo`. It ignores any other query parameter, so a misspelt filter returns every order. Send each filter once: a parameter sent twice matches nothing.

    <Tip>
      To change the price, amount, expiry, slippage or gas price of a `pending` order, send only those fields to [`PATCH /limit-order/{id}`](/api-reference/limit-orders/update-limit-order). Send `price` and `priceTrigger` together: a `PATCH` with only `priceTrigger` leaves `price` unchanged. If you change `amount`, change the allowance with it. To change the tokens, the chain or the maker, cancel the order and create a new one. On an order that is no longer `pending`, the `PATCH` fails with `409 CONFLICT`.
    </Tip>
  </Step>

  <Step title="Cancel the order and lower the allowance">
    [`DELETE /limit-order/{id}`](/api-reference/limit-orders/cancel-limit-order) cancels a `pending` order: its `status` becomes `cancelled` and `deletedAt` records the time of the call. The order stays readable and stays in the list. A `DELETE` on an order that is no longer `pending`, including one you already cancelled, fails with `409 CONFLICT`.

    Cancelling doesn't touch your allowance, and the pair signature stays valid. Lower the allowance by this order's reserve, and keep what your other open orders need:

    ```ts theme={null}
    if (final.status === "pending") {
      final = await olympexRequest<LimitOrder>("DELETE", `/limit-order/${final.id}`);
      console.log(`Cancelled at ${final.deletedAt}`);
    }
    if (final.status === "cancelled") {
      const current: bigint = await sellToken.allowance(maker, orderContract);
      await setAllowance(current > reserve ? current - reserve : 0n);
    }
    ```

    If execution starts after your last poll, the `DELETE` fails with `409 CONFLICT`: read the order again and handle its new status. When an order completes or fails, set the allowance to what your remaining open orders and active DCA strategies need. To stop everything that sells WETH on Polygon at once, set the allowance to `0`.
  </Step>
</Steps>

## Verify

Run these at the end of the script:

```ts theme={null}
const check = await olympexRequest<LimitOrder>("GET", `/limit-order/${created.id}`);
console.log(check.status, check.deletedAt); // cancelled, then an ISO 8601 time

const listed = await olympexRequest<LimitOrder[]>("GET", `/limit-order?accountTo=${accountTo}&chainId=${CHAIN_ID}`);
console.log(listed.some((o) => o.id === created.id)); // true: the list includes cancelled orders

console.log((await sellToken.allowance(maker, orderContract)) === allowanceBefore); // true after a cancel
```

* The order is `cancelled`, with `deletedAt` set, and it still appears in the list.
* The same list request with the maker in lowercase returns `[]`: the `accountTo` filter matches case-sensitively.
* The allowance is back to its value before step 4.

## Common pitfalls

<Warning>
  **Signing the hex string.** Sign the 32 bytes of the hash (`getBytes(inner)`), not its hex string. Olympex accepts either when you create the order, but a signature over the string makes execution fail. Check every signature with `verifyMessage` before you send it.
</Warning>

<Warning>
  **An allowance that covers only `amount`.** The fee is taken in the token sold, on top of `amount`. Approve `amount` plus the fee estimate with a buffer, or the order can't execute. The quote's estimate leaves out the Olympex contracts, so don't approve it without a buffer.
</Warning>

<Warning>
  **Wrong token symbols.** `tokenASymbol` and `tokenBSymbol` must be the tokens' real symbols. Olympex doesn't check them against the token contracts: a real but wrong symbol, such as `WBTC` on a WETH order, is accepted, and the order is priced from that token's market. Drop list suffixes such as `_1`, and check the symbols the response returns.
</Warning>

<Warning>
  **Overwriting a shared allowance.** Limit orders and DCA strategies that sell the same token on the same chain share one allowance. Approve the sum they need: approving one order's amount on its own takes the allowance away from the others.
</Warning>

<Warning>
  **Retrying a create.** Each successful `POST /limit-order` creates a new order, and a timeout doesn't tell you whether one was created. List your orders before you send it again, or you can end up with two live orders on the same allowance.
</Warning>

<Warning>
  **Tokens the order contract can't sell.** Native tokens (wrap them first), USDT on Ethereum and fee-on-transfer tokens can't be the token you sell.
</Warning>

<Warning>
  **An unlimited approval.** The pair signature stays valid after you cancel, so the allowance is the only on-chain limit on what Olympex can move from the wallet. Keep it to what your open orders need.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="Limit orders" icon="book" href="/concepts/limit-orders">
    Order terms, statuses and the reference price.
  </Card>

  <Card title="Order signatures and allowances" icon="shield-halved" href="/concepts/order-authorization">
    What the signature and the allowance let Olympex do.
  </Card>

  <Card title="Run a DCA strategy" icon="calendar" href="/guides/build-a-dca-strategy">
    Spend a token in equal orders over time.
  </Card>

  <Card title="Create a limit order reference" icon="code" href="/api-reference/limit-orders/create-limit-order">
    Every field of `POST /limit-order`.
  </Card>
</CardGroup>


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