- Fetches a quote
- Applies slippage protection
- Signs the order (EIP-712)
- Submits it to the order book
- 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, andeth-accountinstalled
| Network | Base URL |
|---|---|
| Ethereum Mainnet | https://api.cow.fi/mainnet/api/v1 |
| Gnosis Chain | https://api.cow.fi/xdai/api/v1 |
| Arbitrum One | https://api.cow.fi/arbitrum_one/api/v1 |
| Base | https://api.cow.fi/base/api/v1 |
| Sepolia (testnet) | https://api.cow.fi/sepolia/api/v1 |
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
Send a trade intention to
POST /api/v1/quote. The API returns estimated amounts and fee information.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']}")
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.For sell orders, the
buyAmount from the quote is an estimate. Apply a slippage tolerance to set the minimum amount your agent will accept.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}")
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.
CoW Protocol orders are off-chain intents signed with EIP-712. The key points for the signed order:
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 2The 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.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}")
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.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.
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}")
Agent-specific considerations
Rate limiting
The CoW Protocol API enforces per-IP rate limits. Key limits for agent workflows:| Endpoint | Limit |
|---|---|
POST /quote | 10 req/s |
POST /orders | 5 req/s |
GET /orders/{uid} | 100 req/min |
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
validTohas 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 code | Meaning | Agent action |
|---|---|---|
200 | Success | Proceed normally |
400 | Bad request (invalid parameters, expired quote, insufficient balance) | Parse the error body, fix the issue, and retry. Common errors: InsufficientBalance, InsufficientAllowance, ExpiredQuote. |
429 | Rate limited | Read the Retry-After header, wait that duration plus random jitter, then retry. Max 3 retries. |
403 | Cloudflare WAF block | Your IP has been flagged. Do not retry in a loop — this will make it worse. See Rate Limits. |
500 | Server error | Wait 5 seconds and retry. Max 3 retries. If persistent, back off longer. |
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)
Complete example
A minimal Python script that runs the full flow:Next steps
- Rate Limits & Quotas — full rate limit details and backoff strategies
- How Intents Are Formed — understand the fee pipeline and amount calculations
- Signing Schemes — EIP-712,
ethSign, ERC-1271, and PreSign options - TypeScript SDK — higher-level SDK with built-in rate limiting
- cow-py SDK — Python SDK with
swap_tokens()and automatic backoff - API Integration Guide — complete REST API reference