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

# Run a DCA strategy

> Approve the Olympex order contract, sign the token pair, create a DCA strategy, follow its orders, then cancel it and lower the allowance.

A DCA (dollar-cost averaging) strategy spends `totalAmount` of one token in `iterations` equal orders, one every `frequency` seconds, buying another token. Olympex creates each order as the strategy runs and executes it from the maker's wallet. This guide spends 100 USDC on WETH on Polygon in 10 daily orders of 10 USDC, follows the orders, then cancels the strategy 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 USDC 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 USDC to spend 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 new strategy starts `active` as soon as you create it, and each of its orders moves funds on mainnet. Use a small amount from a dedicated wallet. To test the calls without scheduling orders, create the strategy with `"status": "cancelled"` (step 5).
</Warning>

<Steps>
  <Step title="Set up and check the chain">
    The snippets in these steps form one script, `run-dca-strategy.ts`. Run it with `node run-dca-strategy.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}
    // run-dca-strategy.ts. Run: node run-dca-strategy.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 USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"; // the token you spend
    const WETH = "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"; // the token you buy
    const plan = { totalAmount: 100, iterations: 10, frequency: 86_400, slippage: 1 }; // 10 orders of 10 USDC, one a day, 1% slippage

    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 DCA on chain ${CHAIN_ID}`);
    ```

    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 from = findToken(USDC); // symbol "USDC", decimals 6
    const to = findToken(WETH); // symbol "WETH", decimals 18
    ```

    The list holds hundreds of tokens and changes rarely, so cache it on your side, for example for 24 hours. A listed symbol can differ from the token contract's `symbol()`, for example with a suffix such as `STRK_1` that tells tokens with the same symbol apart, so `findToken` trims it and drops a trailing `_<number>`.

    The token you spend must be an ERC-20. To spend 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 spent 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="Approve the order contract for totalAmount">
    Each order pulls its amount from the maker through the Olympex order contract. Olympex takes the execution gas out of the token bought, so the allowance covers `totalAmount` of the token you spend, with nothing on top.

    Read the wallet's current allowance for the order contract, then approve that value plus `totalAmount`. 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 strategy's amount would take it away from the others.

    ```ts theme={null}
    /** 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;
    };

    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 fromToken = new Contract(from.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 spent to exactly `value`. */
    const setAllowance = async (value: bigint): Promise<void> => {
      const current: bigint = await fromToken.allowance(maker, orderContract);
      if (current === value) return;
      if (RESET_BEFORE_APPROVE && current > 0n && value > 0n) await (await fromToken.approve(orderContract, 0n)).wait();
      await (await fromToken.approve(orderContract, value)).wait(); // wait() throws if the approval reverts
    };

    const total = toBaseUnitsUp(plan.totalAmount, from.decimals); // 100 USDC is 100000000 base units
    const balance: bigint = await fromToken.balanceOf(maker);
    if (balance < total) console.warn(`The wallet holds ${formatUnits(balance, from.decimals)} ${from.symbol}: fund it before the later orders run`);

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

    Nothing moves when you approve or create the strategy. The allowance is the on-chain limit on what Olympex can move from the wallet: approve only what your open orders and strategies 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`, the same file [Place a limit order](/guides/create-a-limit-order) uses:

    ```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 USDC for WETH:

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

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

    Olympex doesn't check the signature when you create the strategy: a wrong one makes execution fail later, which the self-check in `signOrderPair` prevents.

    The signature lets the Olympex order contract swap USDC for WETH 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 it's the same signature a limit order that sells USDC for WETH uses.
  </Step>

  <Step title="Create the strategy with POST /dca-order/strategies">
    [`POST /dca-order/strategies`](/api-reference/dca/create-dca-strategy) stores the strategy. It starts `active` immediately.

    | Field | What to send |
    | - | - |
    | `accountTo` | The maker, in its EIP-55 checksummed form. List filters match it case-sensitively. |
    | `chainIdFrom`, `chainIdTo` | The chain, as a number. They must be equal: a strategy runs on one chain. |
    | `tokenAddressFrom`, `tokenAddressTo` | The token you spend (an ERC-20) and the token you buy. |
    | `tokenSymbolFrom`, `tokenSymbolTo`, `pair` | The two symbols, and the pair as `"<tokenSymbolFrom>/<tokenSymbolTo>"`, for example `"USDC/WETH"`. `pair` isn't returned. |
    | `totalAmount` | Total to spend across all orders, human-readable (`100` is 100 USDC), as a JSON number. Each order spends `totalAmount / iterations`. |
    | `frequency`, `iterations` | Seconds between orders (`86400` is daily), and the number of orders. |
    | `slippage` | Maximum slippage per order, in percent, as a JSON number (`1` is 1%). Always send it. |
    | `minPrice`, `maxPrice` | Optional bounds, in units of `tokenAddressTo` per 1 `tokenAddressFrom`. Olympex executes an order only while the price is within the bounds you set. |
    | `signature` | From the previous step. |
    | `status` | Optional. Omit it and the strategy starts `active`. Send `"cancelled"` to create a stopped strategy, for example to test your integration without scheduling orders. |

    Olympex checks the types, the required fields and that each address is a valid EVM address. It doesn't check the other rules in this table or the pair signature: a strategy that breaks one is created, and its orders can't execute. Check them before you send the request. Other fields in the schema are set by Olympex; don't send them.

    ```ts theme={null}
    type DcaStrategy = {
      id: string;
      status: string; // active, cancelled or finished
      tokenAddressFrom: string;
      tokenAddressTo: string;
      totalAmount: number;
      frequency: number;
      iterations: number;
      createdAt: string;
    };

    const sentAt = Date.now();
    const created = await olympexRequest<DcaStrategy>("POST", "/dca-order/strategies", {
      accountTo,
      chainIdFrom: CHAIN_ID,
      chainIdTo: CHAIN_ID,
      tokenAddressFrom: getAddress(from.address),
      tokenAddressTo: getAddress(to.address),
      tokenSymbolFrom: from.symbol,
      tokenSymbolTo: to.symbol,
      pair: `${from.symbol}/${to.symbol}`, // "USDC/WETH"
      totalAmount: plan.totalAmount, // JSON numbers here, not strings
      frequency: plan.frequency,
      iterations: plan.iterations,
      slippage: plan.slippage,
      signature,
    });
    console.log(`Strategy ${created.id} is ${created.status}`); // store the ID now
    ```

    ```json Response theme={null}
    {
      "success": true,
      "data": {
        "id": "320263ca-2f38-4349-b641-a23aed2ff847",
        "accountTo": "0x1E67cb01969D79B2B895179e4A07D24a839dBb52",
        "chainIdFrom": 137,
        "chainIdTo": 137,
        "tokenAddressFrom": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
        "tokenAddressTo": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
        "tokenSymbolFrom": "USDC",
        "tokenSymbolTo": "WETH",
        "totalAmount": 100,
        "frequency": 86400,
        "iterations": 10,
        "slippage": 1,
        "status": "active",
        "createdAt": "2026-09-29T13:48:57.833Z",
        "updatedAt": "2026-09-29T13:48:57.833Z"
      },
      "meta": {
        "requestId": "Eds3EgIrIAMEPJA=",
        "version": "v1",
        "accountType": "integrator",
        "apiKeyId": "00000000-0000-4000-8000-000000000000"
      }
    }
    ```

    Each successful call creates a new strategy, and Olympex ignores any `id` you send. If the call times out or fails with a 5xx, don't send it again: the strategy may exist. [`GET /dca-order/strategies`](/api-reference/dca/list-dca-strategies) lists your strategies newest first, so look there first:

    ```ts theme={null}
    const CLOCK_SKEW_MS = 60_000; // your tolerance for clock differences between your server and Olympex

    /** After a timeout or 5xx on the create, look for the strategy before you send it again. */
    const findCreatedStrategy = async (): Promise<DcaStrategy | undefined> =>
      (await olympexRequest<DcaStrategy[]>("GET", `/dca-order/strategies?accountTo=${accountTo}`)).find(
        (s) =>
          s.tokenAddressFrom === getAddress(from.address) &&
          s.tokenAddressTo === getAddress(to.address) &&
          s.totalAmount === plan.totalAmount &&
          s.iterations === plan.iterations &&
          s.frequency === plan.frequency &&
          Date.parse(s.createdAt) >= sentAt - CLOCK_SKEW_MS,
      );
    ```

    The strategy list filters on `accountTo` and `status` only. It ignores any other query parameter, such as `chainId`, so a misspelt filter returns every strategy. Send each parameter once: a parameter sent twice returns an empty array.
  </Step>

  <Step title="Follow the orders">
    Olympex places the orders one `frequency` apart. A new strategy has no orders until its first order is scheduled. Olympex doesn't notify you when an order changes, so check on your own schedule:

    * [`GET /dca-order/strategies/{id}`](/api-reference/dca/get-dca-strategy) returns the strategy's `status`, without its orders. `finished` means every order ran.
    * [`GET /dca-order/strategies/{id}/orders`](/api-reference/dca/list-dca-strategy-orders) returns the orders Olympex has created so far, oldest first.

    | Order `status` | Meaning |
    | - | - |
    | `pending` | Scheduled. |
    | `executing` | In progress. It can't be stopped. |
    | `successful` | Executed. `transactionHash` is the execution transaction, `amountReceived` the amount of the token bought, and `executionPrice` the price in units of the token spent per 1 token bought. |
    | `cancelled` | Skipped. |
    | `error` | Failed. `errorMessage` says why. |
    | `expired` | Not executed in time. |

    Treat any other value as not final.

    ```ts theme={null}
    type DcaOrder = {
      id: string;
      amount: number; // of the token spent, human-readable
      status: string;
      amountReceived?: number;
      executionPrice?: number;
      transactionHash?: string;
      errorMessage?: string;
    };

    /** Prints the strategy's status and each order's outcome, and returns the orders. */
    const checkStrategy = async (id: string): Promise<DcaOrder[]> => {
      const strategy = await olympexRequest<DcaStrategy>("GET", `/dca-order/strategies/${id}`);
      const orders = await olympexRequest<DcaOrder[]>("GET", `/dca-order/strategies/${id}/orders`);
      console.log(`Strategy ${strategy.status}: ${orders.length} of ${strategy.iterations} orders created`);
      for (const o of orders) {
        if (o.status === "successful") console.log(`${o.amount} ${from.symbol} bought ${o.amountReceived} ${to.symbol} in ${o.transactionHash}`);
        else if (o.status === "error") console.log(`Order ${o.id} failed: ${o.errorMessage}`);
        else console.log(`Order ${o.id} is ${o.status}`);
      }
      return orders;
    };

    await checkStrategy(created.id);
    ```

    A response from the orders endpoint, after the first order ran:

    ```json Response theme={null}
    {
      "success": true,
      "data": [
        {
          "id": "5b3e1f7a-9c2d-4e8b-a1f0-6d4c2b8e9a17",
          "strategyId": "320263ca-2f38-4349-b641-a23aed2ff847",
          "accountTo": "0x1E67cb01969D79B2B895179e4A07D24a839dBb52",
          "amount": 10,
          "amountReceived": 0.00238,
          "executionPrice": 4201.68,
          "status": "successful",
          "transactionHash": "0x9f2c4b1e7a3d5c8f0b6e2a4d1c7f9e3b5a8d0c2e4f6a1b3d5c7e9f0a2b4c6d8e",
          "createdAt": "2026-09-30T12:52:10.004Z",
          "updatedAt": "2026-09-30T12:52:41.377Z"
        }
      ],
      "meta": {
        "requestId": "Edkf_izPoAMEPqg=",
        "version": "v1",
        "accountType": "integrator",
        "apiKeyId": "00000000-0000-4000-8000-000000000000"
      }
    }
    ```

    Route these `GET` requests through your retry wrapper: `GET` is safe to repeat.

    <Tip>
      To change the price bounds of a running strategy, send only `minPrice` or `maxPrice` to [`PATCH /dca-order/strategies/{id}`](/api-reference/dca/update-dca-strategy). To change anything else, cancel the strategy and create a new one.
    </Tip>
  </Step>

  <Step title="Cancel the strategy">
    Send `{"status":"cancelled"}` to [`PATCH /dca-order/strategies/{id}`](/api-reference/dca/update-dca-strategy). Cancelling stops new orders and can't be undone. Orders that Olympex already created are read-only: the next step lowers the allowance so they can't spend what you release.

    ```ts theme={null}
    const cancelled = await olympexRequest<DcaStrategy>("PATCH", `/dca-order/strategies/${created.id}`, { status: "cancelled" });
    console.log(`Strategy ${cancelled.status}`); // cancelled
    ```

    Sending the same `PATCH` again is safe.
  </Step>

  <Step title="Lower the allowance">
    Cancelling doesn't touch your allowance, and the pair signature stays valid. Lower the allowance by the part of `totalAmount` the strategy won't spend, and keep what your other open orders need:

    ```ts theme={null}
    const orders = await olympexRequest<DcaOrder[]>("GET", `/dca-order/strategies/${created.id}/orders`);
    // Successful orders have used their amount, and an executing order can still use it. Release the rest.
    const used = orders
      .filter((o) => o.status === "successful" || o.status === "executing")
      .reduce((sum, o) => sum + toBaseUnitsUp(o.amount, from.decimals), 0n);
    const unused = total > used ? total - used : 0n;

    const current: bigint = await fromToken.allowance(maker, orderContract);
    await setAllowance(current > unused ? current - unused : 0n);
    ```

    If an order that was `executing` ends in `error`, lower the allowance by its amount too. When a strategy finishes, its orders have used their share of the allowance. To stop everything that spends USDC on Polygon at once, set the allowance to `0`: that also stops every limit order that sells USDC there.
  </Step>
</Steps>

## Verify

Run these at the end of the script:

```ts theme={null}
console.log((await olympexRequest<DcaStrategy>("GET", `/dca-order/strategies/${created.id}`)).status); // cancelled

const listed = await olympexRequest<DcaStrategy[]>("GET", `/dca-order/strategies?accountTo=${accountTo}&status=cancelled`);
console.log(listed.some((s) => s.id === created.id)); // true

console.log(formatUnits(await fromToken.allowance(maker, orderContract), from.decimals));
```

* The strategy is `cancelled`.
* It appears in the strategy list filtered by the checksummed maker and `status=cancelled`. With the maker in lowercase, the list is empty: `accountTo` matches case-sensitively.
* Once no order is `executing`, the allowance is back to its value before step 3: executed orders used their part, and you released the rest.

## Common pitfalls

<Warning>
  **Strings in numeric fields.** `totalAmount`, `slippage`, `minPrice`, `maxPrice`, `frequency`, `iterations`, `chainIdFrom` and `chainIdTo` are JSON numbers, unlike limit-order fields. A string returns `400 VALIDATION_ERROR`.
</Warning>

<Warning>
  **Two different chains.** `chainIdFrom` must equal `chainIdTo`: a strategy runs on one chain.
</Warning>

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

<Warning>
  **Retrying a create.** Each successful `POST /dca-order/strategies` creates a new strategy that starts `active`. After a timeout, list your strategies before you send it again, or two strategies spend from the same allowance.
</Warning>

<Warning>
  **Stopping at the strategy.** Cancelling a strategy stops new orders, not the orders Olympex already created, which are read-only. Lower the allowance too.
</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 and strategies need.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="DCA" icon="book" href="/concepts/dca">
    Strategies, orders, price bounds and statuses.
  </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="Place a limit order" icon="bullseye" href="/guides/create-a-limit-order">
    Sell a token when the market reaches your price.
  </Card>

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


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