Skip to main content
This tutorial shows how an autonomous AI agent (running in a loop, triggered by events, or driven by an LLM) can create and manage orders on CoW Protocol through the REST API. By the end you will have a working Python script that:
  1. Fetches a quote
  2. Applies slippage protection
  3. Signs the order (EIP-712)
  4. Submits it to the order book
  5. Monitors execution with proper error recovery
This guide builds on the same API flow as the Raw API (cURL) and Python quickstarts. If you have not placed an order before, start there.

Prerequisites

Before starting, make sure your agent has access to:
  • A private key for an EOA wallet (stored securely — environment variable or secrets manager)
  • ETH on the target chain — only needed for token approval transactions, not for order submission
  • Tokens to trade — the sell token must already be in the wallet. Any ERC-20 token is tradeable, not just those on the CoW token list (the list is used by the UI for display purposes)
  • Token approval for the GPv2VaultRelayer (0xC92E8bdf79f0507f65a392b0ab4667716BFE0110) to spend the sell token
  • Python 3.8+ with requests, web3, and eth-account installed
API base URLs by network:
NetworkBase URL
Ethereum Mainnethttps://api.cow.fi/mainnet/api/v1
Gnosis Chainhttps://api.cow.fi/xdai/api/v1
Arbitrum Onehttps://api.cow.fi/arbitrum_one/api/v1
Basehttps://api.cow.fi/base/api/v1
Sepolia (testnet)https://api.cow.fi/sepolia/api/v1
Never hardcode private keys. Use os.environ.get("PRIVATE_KEY") in Python (not string interpolation or shell expansion) to avoid exposing secrets in process argument lists visible via ps. Avoid passing keys as command-line arguments (e.g., python script.py --key 0x...). For production agents, use a secrets manager.
Want to trade native ETH (not WETH)? You can use Eth-flow to place orders selling native ETH directly, without wrapping to WETH first.

Walkthrough

1
Step 1: Get a quote
2
Send a trade intention to POST /api/v1/quote. The API returns estimated amounts and fee information.
3
import os
import time
import random
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import Web3

# --------------- Configuration ---------------
PRIVATE_KEY = os.environ["PRIVATE_KEY"]
API_BASE = "https://api.cow.fi/sepolia/api/v1"
CHAIN_ID = 11155111  # Sepolia

SETTLEMENT_CONTRACT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
VAULT_RELAYER = "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110"

SELL_TOKEN = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"  # WETH
BUY_TOKEN = "0xbe72E441BF55620febc26715db68d3494213D8Cb"   # COW

account = Account.from_key(PRIVATE_KEY)

# --------------- Get a quote ---------------
def get_quote(sell_token, buy_token, sell_amount, sender):
    """Request a quote from the CoW Protocol API."""
    quote_request = {
        "sellToken": sell_token,
        "buyToken": buy_token,
        "sellAmountBeforeFee": str(sell_amount),
        "kind": "sell",
        "from": sender,
        "receiver": sender,
        "validFor": 600,  # 10 minutes — shorter validity for agents
        "signingScheme": "eip712",
    }

    resp = requests.post(f"{API_BASE}/quote", json=quote_request)
    resp.raise_for_status()
    return resp.json()

quote_data = get_quote(SELL_TOKEN, BUY_TOKEN, 10**17, account.address)
quote = quote_data["quote"]

print(f"Sell amount (after network costs): {quote['sellAmount']}")
print(f"Buy amount (estimated):            {quote['buyAmount']}")
print(f"Fee amount (network costs):        {quote['feeAmount']}")
print(f"Valid until:                        {quote['validTo']}")
4
Use a shorter validFor (e.g. 600 seconds) for agent workflows. The quote expires after this period — if your agent takes too long between quoting and signing, the order will be rejected.
5
Step 2: Apply slippage
6
For sell orders, the buyAmount from the quote is an estimate. Apply a slippage tolerance to set the minimum amount your agent will accept.
7
SLIPPAGE_BPS = 50  # 0.5% slippage tolerance

def apply_slippage(buy_amount, slippage_bps):
    """Reduce buyAmount by slippage tolerance to get the minimum acceptable amount."""
    return int(int(buy_amount) * (10000 - slippage_bps) / 10000)

min_buy_amount = apply_slippage(quote["buyAmount"], SLIPPAGE_BPS)
print(f"Min buy amount (with {SLIPPAGE_BPS/100}% slippage): {min_buy_amount}")
8
Setting slippage too tight (e.g. 0.01%) causes orders to fail in volatile markets. Setting it too loose (e.g. 5%) exposes your agent to worse execution. 0.5% is a reasonable default for most liquid pairs.
9
Step 3: Sign the order
10
CoW Protocol orders are off-chain intents signed with EIP-712. The key points for the signed order:
11
  • feeAmount = 0 (fees are handled by the settlement contract, always sign with zero)
  • sellAmount = quote.sellAmount + quote.feeAmount (add the fee back to get the full pre-fee amount)
  • buyAmount = the slippage-adjusted minimum from Step 2
  • 12
    The correct sequence is: (1) add feeAmount back to sellAmount, (2) apply slippage tolerance to buyAmount, (3) apply partner fee if applicable. Always sign with feeAmount: 0.
    13
    Python
    # EIP-712 domain
    domain_data = {
        "name": "Gnosis Protocol",
        "version": "v2",
        "chainId": CHAIN_ID,
        "verifyingContract": SETTLEMENT_CONTRACT,
    }
    
    # Order type definition — must match the contract exactly
    order_types = {
        "Order": [
            {"name": "sellToken", "type": "address"},
            {"name": "buyToken", "type": "address"},
            {"name": "receiver", "type": "address"},
            {"name": "sellAmount", "type": "uint256"},
            {"name": "buyAmount", "type": "uint256"},
            {"name": "validTo", "type": "uint32"},
            {"name": "appData", "type": "bytes32"},
            {"name": "feeAmount", "type": "uint256"},
            {"name": "kind", "type": "string"},
            {"name": "partiallyFillable", "type": "bool"},
            {"name": "sellTokenBalance", "type": "string"},
            {"name": "buyTokenBalance", "type": "string"},
        ]
    }
    
    # Build signed order from quote
    sell_amount_to_sign = int(quote["sellAmount"]) + int(quote["feeAmount"])
    
    order_data = {
        "sellToken": quote["sellToken"],
        "buyToken": quote["buyToken"],
        "receiver": quote["receiver"],
        "sellAmount": sell_amount_to_sign,
        "buyAmount": min_buy_amount,
        "validTo": quote["validTo"],
        "appData": bytes.fromhex(quote["appData"][2:]),
        "feeAmount": 0,
        "kind": "sell",
        "partiallyFillable": False,
        "sellTokenBalance": "erc20",
        "buyTokenBalance": "erc20",
    }
    
    signable = encode_typed_data(domain_data, order_types, order_data)
    signed = account.sign_message(signable)
    signature = signed.signature.hex()
    
    print(f"Order signed: 0x{signature[:16]}...")
    
    TypeScript (ethers.js)
    import { ethers } from "ethers";
    
    const domain = {
      name: "Gnosis Protocol",
      version: "v2",
      chainId: 11155111,
      verifyingContract: "0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
    };
    
    const types = {
      Order: [
        { name: "sellToken", type: "address" },
        { name: "buyToken", type: "address" },
        { name: "receiver", type: "address" },
        { name: "sellAmount", type: "uint256" },
        { name: "buyAmount", type: "uint256" },
        { name: "validTo", type: "uint32" },
        { name: "appData", type: "bytes32" },
        { name: "feeAmount", type: "uint256" },
        { name: "kind", type: "string" },
        { name: "partiallyFillable", type: "bool" },
        { name: "sellTokenBalance", type: "string" },
        { name: "buyTokenBalance", type: "string" },
      ],
    };
    
    const order = {
      sellToken: quote.sellToken,
      buyToken: quote.buyToken,
      receiver: quote.receiver,
      sellAmount: (BigInt(quote.sellAmount) + BigInt(quote.feeAmount)).toString(),
      buyAmount: minBuyAmount.toString(),
      validTo: quote.validTo,
      appData: quote.appData,
      feeAmount: "0",
      kind: "sell",
      partiallyFillable: false,
      sellTokenBalance: "erc20",
      buyTokenBalance: "erc20",
    };
    
    const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
    const signature = await wallet.signTypedData(domain, types, order);
    
    14
    Step 4: Submit the order
    15
    Post the signed order to POST /api/v1/orders. The API returns an order UID.
    16
    def submit_order(quote, sell_amount, buy_amount, signature, sender):
        """Submit a signed order to the CoW Protocol order book."""
        body = {
            "sellToken": quote["sellToken"],
            "buyToken": quote["buyToken"],
            "receiver": quote["receiver"],
            "sellAmount": str(sell_amount),
            "buyAmount": str(buy_amount),
            "validTo": quote["validTo"],
            "appData": quote["appData"],
            "feeAmount": "0",
            "kind": "sell",
            "partiallyFillable": False,
            "sellTokenBalance": "erc20",
            "buyTokenBalance": "erc20",
            "signingScheme": "eip712",
            "signature": "0x" + signature,
            "from": sender,
        }
    
        resp = requests.post(f"{API_BASE}/orders", json=body)
        resp.raise_for_status()
        return resp.json()  # returns the order UID string
    
    order_uid = submit_order(
        quote, sell_amount_to_sign, min_buy_amount, signature, account.address
    )
    print(f"Order UID: {order_uid}")
    print(f"Explorer:  https://explorer.cow.fi/sepolia/orders/{order_uid}")
    
    17
    appData differs between endpoints. POST /api/v1/quote accepts the full JSON metadata document as appData and its keccak256 hash as appDataHash. POST /api/v1/orders expects only the bytes32 hash as appData. The quote response returns appData already as the hash, so you can pass it through to the order submission unchanged (as shown above). If you construct appData yourself, make sure you submit the hash — not the raw JSON — when posting orders. See the appData documentation for details.
    18
    The order UID is deterministic — it is derived from the order parameters and the signer address. Submitting the same signed order twice returns the same UID and does not create a duplicate. This gives you built-in idempotency.
    19
    Step 5: Monitor execution
    20
    Poll GET /api/v1/orders/{uid} to track the order lifecycle.
    21
    def wait_for_order(order_uid, timeout_seconds=300, poll_interval=10):
        """Poll order status until it reaches a terminal state."""
        deadline = time.time() + timeout_seconds
    
        while time.time() < deadline:
            resp = requests.get(f"{API_BASE}/orders/{order_uid}")
            resp.raise_for_status()
            status = resp.json()["status"]
            print(f"  Status: {status}")
    
            if status in ("fulfilled", "expired", "cancelled"):
                return status
    
            time.sleep(poll_interval)
    
        return "timeout"
    
    final_status = wait_for_order(order_uid)
    
    if final_status == "fulfilled":
        print("Order filled successfully.")
    elif final_status == "expired":
        print("Order expired — re-quote and retry.")
    else:
        print(f"Final status: {final_status}")
    
    22
    Possible status values:
    23
    StatusMeaningopenIn the order book, awaiting solver executionfulfilledSettled on-chainexpiredvalidTo timestamp passed without settlementcancelledCancelled by the owner

    Agent-specific considerations

    Rate limiting

    The CoW Protocol API enforces per-IP rate limits. Key limits for agent workflows:
    EndpointLimit
    POST /quote10 req/s
    POST /orders5 req/s
    GET /orders/{uid}100 req/min
    Always implement exponential backoff with jitter for 429 responses. See the full Rate Limits & Quotas reference for details and backoff code examples.

    Quote freshness

    Quotes reflect current market conditions and expire. For agents:
    • Re-quote if more than 30 seconds have passed since the last quote, even if the quote’s validTo has not been reached. Market prices can shift significantly in that time.
    • CoW Protocol runs batch auctions roughly every 15 seconds. Quoting more frequently than that provides no benefit.
    • Cache the quote response and only refresh when your agent’s input parameters (token pair, amount) change or the 30-second window elapses.

    Error recovery

    Your agent should handle these HTTP responses:
    Status codeMeaningAgent action
    200SuccessProceed normally
    400Bad request (invalid parameters, expired quote, insufficient balance)Parse the error body, fix the issue, and retry. Common errors: InsufficientBalance, InsufficientAllowance, ExpiredQuote.
    429Rate limitedRead the Retry-After header, wait that duration plus random jitter, then retry. Max 3 retries.
    403Cloudflare WAF blockYour IP has been flagged. Do not retry in a loop — this will make it worse. See Rate Limits.
    500Server errorWait 5 seconds and retry. Max 3 retries. If persistent, back off longer.
    def api_request(method, url, json=None, max_retries=3):
        """Make an API request with exponential backoff for rate limits and errors."""
        for attempt in range(max_retries):
            resp = requests.request(method, url, json=json)
    
            if resp.status_code == 200 or resp.status_code == 201:
                return resp
    
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", 2 ** attempt))
                time.sleep(retry_after + random.random())
                continue
    
            if resp.status_code >= 500:
                time.sleep(5 * (attempt + 1) + random.random())
                continue
    
            # 4xx errors (except 429) — do not retry, raise immediately
            resp.raise_for_status()
    
        raise Exception(f"Failed after {max_retries} retries: {url}")
    

    Idempotency

    Order UIDs are deterministic — derived from the order parameters and signer address. Submitting the same signed order twice returns the same UID without creating a duplicate. This means:
    • Your agent can safely retry a failed submission without risking double-execution.
    • If your agent crashes between signing and confirming submission, it can resubmit the same signed payload on restart.

    Gas management

    CoW Protocol orders are off-chain intents. Submitting and monitoring orders requires zero gas. Your agent only needs ETH for:
    • Token approvals — a one-time approve() transaction per sell token for the GPv2VaultRelayer
    • On-chain cancellations — if you cancel via the settlement contract instead of the API (optional)
    For most agent workflows, a single approval per token is sufficient. The agent itself does not pay gas for trade execution — solvers handle that.

    Complete example

    A minimal Python script that runs the full flow:
    """
    Minimal CoW Protocol agent order — full flow from quote to fill.
    
    Usage:
      export PRIVATE_KEY="0x..."
      python agent_order.py
    """
    
    import os
    import time
    import random
    import requests
    from eth_account import Account
    from eth_account.messages import encode_typed_data
    
    # --------------- Configuration ---------------
    PRIVATE_KEY = os.environ["PRIVATE_KEY"]
    API_BASE = "https://api.cow.fi/sepolia/api/v1"
    CHAIN_ID = 11155111
    SETTLEMENT_CONTRACT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
    
    SELL_TOKEN = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"  # WETH
    BUY_TOKEN = "0xbe72E441BF55620febc26715db68d3494213D8Cb"   # COW
    SELL_AMOUNT = 10**17  # 0.1 WETH
    SLIPPAGE_BPS = 50     # 0.5%
    
    account = Account.from_key(PRIVATE_KEY)
    
    # --------------- Helpers ---------------
    def api_request(method, url, json=None, max_retries=3):
        for attempt in range(max_retries):
            resp = requests.request(method, url, json=json)
            if resp.status_code in (200, 201):
                return resp
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", 2 ** attempt))
                time.sleep(retry_after + random.random())
                continue
            if resp.status_code >= 500:
                time.sleep(5 * (attempt + 1) + random.random())
                continue
            resp.raise_for_status()
        raise Exception(f"Failed after {max_retries} retries: {url}")
    
    
    # --------------- Step 1: Quote ---------------
    quote_resp = api_request("POST", f"{API_BASE}/quote", json={
        "sellToken": SELL_TOKEN,
        "buyToken": BUY_TOKEN,
        "sellAmountBeforeFee": str(SELL_AMOUNT),
        "kind": "sell",
        "from": account.address,
        "receiver": account.address,
        "validFor": 600,
        "signingScheme": "eip712",
    })
    quote = quote_resp.json()["quote"]
    print(f"Quote: sell {quote['sellAmount']}, buy ~{quote['buyAmount']}, fee {quote['feeAmount']}")
    
    # --------------- Step 2: Slippage ---------------
    min_buy = int(int(quote["buyAmount"]) * (10000 - SLIPPAGE_BPS) / 10000)
    sell_amount_to_sign = int(quote["sellAmount"]) + int(quote["feeAmount"])
    
    # --------------- Step 3: Sign ---------------
    domain = {
        "name": "Gnosis Protocol",
        "version": "v2",
        "chainId": CHAIN_ID,
        "verifyingContract": SETTLEMENT_CONTRACT,
    }
    
    order_types = {
        "Order": [
            {"name": "sellToken", "type": "address"},
            {"name": "buyToken", "type": "address"},
            {"name": "receiver", "type": "address"},
            {"name": "sellAmount", "type": "uint256"},
            {"name": "buyAmount", "type": "uint256"},
            {"name": "validTo", "type": "uint32"},
            {"name": "appData", "type": "bytes32"},
            {"name": "feeAmount", "type": "uint256"},
            {"name": "kind", "type": "string"},
            {"name": "partiallyFillable", "type": "bool"},
            {"name": "sellTokenBalance", "type": "string"},
            {"name": "buyTokenBalance", "type": "string"},
        ]
    }
    
    order_data = {
        "sellToken": quote["sellToken"],
        "buyToken": quote["buyToken"],
        "receiver": quote["receiver"],
        "sellAmount": sell_amount_to_sign,
        "buyAmount": min_buy,
        "validTo": quote["validTo"],
        "appData": bytes.fromhex(quote["appData"][2:]),
        "feeAmount": 0,
        "kind": "sell",
        "partiallyFillable": False,
        "sellTokenBalance": "erc20",
        "buyTokenBalance": "erc20",
    }
    
    signable = encode_typed_data(domain, order_types, order_data)
    signed = account.sign_message(signable)
    signature = signed.signature.hex()
    print(f"Signed order: 0x{signature[:16]}...")
    
    # --------------- Step 4: Submit ---------------
    submit_resp = api_request("POST", f"{API_BASE}/orders", json={
        "sellToken": quote["sellToken"],
        "buyToken": quote["buyToken"],
        "receiver": quote["receiver"],
        "sellAmount": str(sell_amount_to_sign),
        "buyAmount": str(min_buy),
        "validTo": quote["validTo"],
        "appData": quote["appData"],
        "feeAmount": "0",
        "kind": "sell",
        "partiallyFillable": False,
        "sellTokenBalance": "erc20",
        "buyTokenBalance": "erc20",
        "signingScheme": "eip712",
        "signature": "0x" + signature,
        "from": account.address,
    })
    order_uid = submit_resp.json()
    print(f"Order UID: {order_uid}")
    print(f"Explorer:  https://explorer.cow.fi/sepolia/orders/{order_uid}")
    
    # --------------- Step 5: Monitor ---------------
    print("Monitoring order...")
    deadline = time.time() + 300  # 5 minute timeout
    
    while time.time() < deadline:
        resp = api_request("GET", f"{API_BASE}/orders/{order_uid}")
        status = resp.json()["status"]
        print(f"  Status: {status}")
    
        if status in ("fulfilled", "expired", "cancelled"):
            break
        time.sleep(10)
    
    if status == "fulfilled":
        print("Order filled successfully.")
    elif status == "expired":
        print("Order expired. Re-quote and retry with fresh market data.")
    else:
        print(f"Final status: {status}")
    

    Next steps

    Last modified on March 12, 2026