- 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",
"appData": "{}",
"appDataHash": "0xb48d38f93eaa084033fc5970bf96e559c33c4cdc07d889ab00b4d63f9590739d",
}
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"]
quote_id = quote_data["id"]
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, quote_id, 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"],
"appDataHash": quote.get("appDataHash"),
"feeAmount": "0",
"kind": "sell",
"partiallyFillable": False,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20",
"signingScheme": "eip712",
"signature": "0x" + signature,
"from": sender,
"quoteId": quote_id,
}
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, quote_id, 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 signing and submission. If the quote returns both quote.appData and quote.appDataHash, sign the hash and submit the JSON appData document together with appDataHash. If you requested a hash-only appData, use that hash value for both signing and submission. 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 the agent delays materially between quoting and submission, even if
validTohas not been reached yet. - Re-quote whenever the agent changes any input that affects execution, such as the token pair, amount, or slippage settings.
- Treat quote data as short-lived state: cache it only within the current decision loop and refresh it before signing if conditions have changed.
Error recovery
Your agent should handle these HTTP responses:| Status code | Meaning | Agent action |
|---|---|---|
200 | Success | Proceed normally |
400 | Bad request (invalid parameters, stale quote, insufficient balance) | Parse the error body, fix the issue, and retry. Common errors: InsufficientBalance, InsufficientAllowance, QuoteNotFound, InvalidQuote. |
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