Skip to main content
This guide walks you through placing a real order on CoW Protocol using Python and the REST API directly. By the end, you will have submitted a signed intent on the Sepolia testnet and watched it get filled.
Prefer a higher-level approach? The cow-py SDK wraps all of this into a single swap_tokens() call with built-in signing, quoting, and order management. This guide is for developers who want to understand the raw API mechanics or who can’t use the SDK.

Prerequisites

Before starting, ensure you have:
  • Python 3.8+ installed
  • A wallet with test tokens on Sepolia (get Sepolia ETH from a faucet, then wrap it to WETH)
  • The wallet’s private key (never use a key that controls real funds for testing)
This guide uses the Sepolia testnet. Never hardcode private keys in production code. Use environment variables or a secrets manager.

Installation

Install the required packages:
pip install web3 requests eth-account

Walkthrough

1
Setup
2
Configure your connection, wallet, and constants.
3
import json
import time
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
from web3 import Web3

# --------------- Configuration ---------------
PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"  # Replace with your Sepolia wallet key
API_BASE = "https://api.cow.fi/sepolia/api/v1"
CHAIN_ID = 11155111  # Sepolia

# Contract addresses (same across all chains unless noted)
SETTLEMENT_CONTRACT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
VAULT_RELAYER = "0xC92E8bdf79f0507f65a392b0ab4667716BFE0110"

# Tokens on Sepolia
SELL_TOKEN = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"  # WETH
BUY_TOKEN = "0xbe72E441BF55620febc26715db68d3494213D8Cb"   # COW

account = Account.from_key(PRIVATE_KEY)
w3 = Web3(Web3.HTTPProvider("https://rpc.sepolia.org"))

print(f"Wallet: {account.address}")
print(f"Balance: {w3.eth.get_balance(account.address)} wei")
4
You can use any Sepolia RPC endpoint — Alchemy, Infura, or the public https://rpc.sepolia.org.
5
Approve tokens
6
CoW Protocol’s settlement contract does not pull tokens directly from your wallet. Instead, the GPv2VaultRelayer contract transfers tokens on your behalf. You must approve it to spend your sell token.
7
If you have already approved the relayer for this token, you can skip this step.
8
# Minimal ERC-20 ABI — only the approve function
erc20_abi = [
    {
        "inputs": [
            {"name": "spender", "type": "address"},
            {"name": "amount", "type": "uint256"},
        ],
        "name": "approve",
        "outputs": [{"name": "", "type": "bool"}],
        "type": "function",
    }
]

token = w3.eth.contract(
    address=Web3.to_checksum_address(SELL_TOKEN), abi=erc20_abi
)

tx = token.functions.approve(
    Web3.to_checksum_address(VAULT_RELAYER),
    2**256 - 1,  # max approval
).build_transaction(
    {
        "from": account.address,
        "nonce": w3.eth.get_transaction_count(account.address),
        "gas": 50_000,
        "gasPrice": w3.eth.gas_price,
    }
)

signed_tx = account.sign_transaction(tx)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

print(f"Approval tx: {tx_hash.hex()}")
print(f"Status: {'success' if receipt.status == 1 else 'failed'}")
9
This issues an unlimited approval for simplicity. In production, approve only the amount you intend to trade.
10
Get a quote
11
Send your trade intention to the /quote endpoint. The API returns estimated amounts and fee information.
12
quote_request = {
    "sellToken": SELL_TOKEN,
    "buyToken": BUY_TOKEN,
    "sellAmountBeforeFee": str(10**17),  # 0.1 WETH
    "kind": "sell",
    "from": account.address,
    "receiver": account.address,
    "validFor": 1800,  # 30 minutes
    "signingScheme": "eip712",
}

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

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']}")
13
The quote is valid for a limited time (controlled by validFor). You must sign and submit the order before it expires. If you get an ExpiredQuote error later, simply request a fresh quote.
14
Sign the order (EIP-712)
15
CoW Protocol orders are intents — signed messages that authorize the protocol to execute the trade on your behalf. Orders are signed using the EIP-712 typed data standard.
16
# EIP-712 domain for CoW Protocol
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 the order data from the quote
# For sell orders: sellAmount = quote.sellAmount + quote.feeAmount (spot price)
#                  buyAmount  = quote.buyAmount (minimum to receive)
order_data = {
    "sellToken": quote["sellToken"],
    "buyToken": quote["buyToken"],
    "receiver": quote["receiver"],
    "sellAmount": int(quote["sellAmount"]) + int(quote["feeAmount"]),
    "buyAmount": int(quote["buyAmount"]),
    "validTo": quote["validTo"],
    "appData": bytes.fromhex(quote["appData"][2:]),
    "feeAmount": 0,
    "kind": "sell",
    "partiallyFillable": False,
    "sellTokenBalance": "erc20",
    "buyTokenBalance": "erc20",
}

# Sign the typed data
signable = encode_typed_data(domain_data, order_types, order_data)
signed = account.sign_message(signable)
signature = signed.signature.hex()

print(f"Order signed successfully")
print(f"Signature: 0x{signature[:16]}...")
17
The feeAmount in the signed order is set to 0 and the sellAmount is set to the spot price amount (quote.sellAmount + quote.feeAmount). The settlement contract deducts network costs from the sell amount automatically. See How Intents Are Formed for details on the fee pipeline.
18
Submit the order
19
Post the signed order to the /orders endpoint. The API returns an order UID that you can use to track execution.
20
submit_body = {
    "sellToken": quote["sellToken"],
    "buyToken": quote["buyToken"],
    "receiver": quote["receiver"],
    "sellAmount": str(int(quote["sellAmount"]) + int(quote["feeAmount"])),
    "buyAmount": quote["buyAmount"],
    "validTo": quote["validTo"],
    "appData": quote["appData"],
    "feeAmount": "0",
    "kind": "sell",
    "partiallyFillable": False,
    "sellTokenBalance": "erc20",
    "buyTokenBalance": "erc20",
    "signingScheme": "eip712",
    "signature": "0x" + signature,
    "from": account.address,
}

response = requests.post(f"{API_BASE}/orders", json=submit_body)
response.raise_for_status()
order_uid = response.json()

print(f"Order UID: {order_uid}")
print(f"Track at:  https://explorer.cow.fi/sepolia/orders/{order_uid}")
21
Monitor the order
22
Poll the API to watch the order progress from open to fulfilled.
23
print("Waiting for order to be filled...")

for _ in range(60):  # poll for up to 5 minutes
    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"):
        break
    time.sleep(5)

if status == "fulfilled":
    print("Order filled successfully!")
elif status == "expired":
    print("Order expired before a solver could fill it. Try again with a fresh quote.")
else:
    print(f"Final status: {status}")

Troubleshooting

ErrorCauseFix
InsufficientAllowanceThe vault relayer is not approved to spend your sell tokenRun the approval step (Step 2)
InvalidSignatureEIP-712 domain or type definitions do not match the contractVerify domain_data and order_types match exactly as shown above
ExpiredQuoteThe quote validity period has elapsedRequest a fresh quote and sign/submit immediately
InsufficientBalanceYour wallet does not hold enough sell tokenGet test tokens from a Sepolia faucet and wrap ETH to WETH
SellAmountDoesNotCoverFeeTrade amount is too small to cover network costsIncrease the sellAmountBeforeFee in your quote request
For a full list of API error codes, see the Order Book API Reference.

Next Steps

Now that you have placed your first order, explore more of CoW Protocol:
Last modified on March 12, 2026