Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cowprotocol/solver-rewards/llms.txt
Use this file to discover all available pages before exploring further.
Every solver that participated in the network during an accounting period receives a payout composed of up to three components: a batch reward (for settling trades), a quote reward (for providing accurate price quotes), and a reimbursement or penalty based on their net slippage. A service fee is deducted from all positive rewards as a cut for CoW DAO.
Payout columns
All solver-level inputs are carried in a DataFrame whose columns are exactly SOLVER_PAYOUTS_COLUMNS:
# src/fetch/payouts.py
SOLVER_PAYOUTS_COLUMNS = [
"solver",
"solver_name",
"primary_reward_eth",
"primary_reward_cow",
"quote_reward_cow",
"protocol_fee_eth",
"network_fee_eth",
"slippage_eth",
"reward_target",
"buffer_accounting_target",
"reward_token_address",
"service_fee",
]
All monetary values are in wei (integer atoms of the respective token). The pipeline asserts that the DataFrame has exactly these columns before processing.
Batch rewards
Batch rewards are earned by settling user trades on-chain. They are stored as two parallel values:
| Column | Type | Meaning |
|---|
primary_reward_eth | int (wei) | Reward denominated in the chain’s native token |
primary_reward_cow | int (atoms) | Reward denominated in COW tokens |
Both values originate from the orderbook analytics database and reflect the value the solver added during the period.
On Mainnet, both columns are populated. On chains where COW is not natively distributed, primary_reward_cow may be zero or use a dummy token address.
Quote rewards
Quote rewards incentivize solvers to provide accurate price quotes to users browsing the CoW Protocol UI. Each valid quote earns a flat reward:
# src/config.py — RewardConfig.from_network()
quote_reward_cow = 6 * 10**18 # 6 COW per valid quote (all networks)
This is stored in SOLVER_PAYOUTS_COLUMNS as quote_reward_cow. Quote rewards are always paid in COW tokens and are always non-negative (asserted in RewardAndPenaltyDatum.__init__).
Per-network caps
To prevent runaway costs, each network enforces a quote_reward_cap_native — the maximum native-token value that can be paid out per quote. If the COW equivalent of 6 COW exceeds this cap at the current exchange rate, the reward is capped:
# src/config.py
case Network.MAINNET:
quote_reward_cow = 6 * 10**18
quote_reward_cap_native = 7 * 10**14 # 0.0007 ETH
case Network.GNOSIS:
quote_reward_cow = 6 * 10**18
quote_reward_cap_native = 15 * 10**16 # 0.15 xDAI
case Network.ARBITRUM_ONE:
quote_reward_cow = 6 * 10**18
quote_reward_cap_native = 24 * 10**13 # 0.00024 ETH
case Network.BASE:
quote_reward_cow = 6 * 10**18
quote_reward_cap_native = 24 * 10**13 # 0.00024 ETH
case Network.AVALANCHE:
quote_reward_cow = 6 * 10**18
quote_reward_cap_native = 6 * 10**15 # 0.006 AVAX
Service fee
CoW DAO retains a 15% cut of all positive rewards as a service fee:
# src/config.py
service_fee_factor = Fraction(15, 100) # 0.15
The factor is stored per-solver in the service_fee column of SOLVER_PAYOUTS_COLUMNS. Solvers that have service_fee_enabled = 0 in the analytics database receive a service_fee of 0 (no cut taken).
reward_scaling()
The scaling multiplier applied to all positive rewards is 1 - service_fee:
# src/fetch/payouts.py
def reward_scaling(self) -> Fraction:
"""Scaling factor for service fee
The reward is multiplied by this factor"""
return 1 - self.service_fee
With the default factor, reward_scaling() returns Fraction(85, 100) — meaning solvers keep 85% of earned rewards and CoW DAO retains 15%.
total_service_fee()
The total service fee charged from a solver’s rewards (in COW atoms) is:
def total_service_fee(self) -> Fraction:
"""Total service fee charged from rewards"""
return self.service_fee * (
max(self.primary_reward_cow, 0) + self.quote_reward_cow
)
Note that service fee is only applied to positive primary_reward_cow. Negative primary rewards (penalties) are not amplified by the service fee.
Total reward calculations
total_cow_reward()
def total_cow_reward(self) -> int:
"""Total outgoing COW token reward"""
return (
int(self.reward_scaling() * self.primary_reward_cow)
if self.primary_reward_cow > 0
else self.primary_reward_cow
)
Service fee scaling is only applied when the primary reward is positive. If primary_reward_cow is zero or negative, it passes through unchanged.
total_eth_reward()
def total_eth_reward(self) -> int:
"""Total outgoing ETH reward"""
return (
int(self.reward_scaling() * self.primary_reward_eth)
if self.primary_reward_eth > 0
else self.primary_reward_eth
)
The same logic applies to native-token rewards.
Dual-token payout logic
The as_payouts() method on RewardAndPenaltyDatum implements the full decision tree for which token(s) to use when constructing Transfer objects. The key insight is that the total outgoing value must remain consistent even when rewards and reimbursements have opposite signs.
def as_payouts(self) -> list[Transfer]:
quote_reward_cow = int(self.reward_scaling() * self.quote_reward_cow)
result = []
# Quote rewards are always paid in COW, regardless of other signs
if quote_reward_cow > 0:
result.append(Transfer(token=Token(self.reward_token_address, 18),
recipient=self.reward_target,
amount_wei=quote_reward_cow))
# Overdrafts are excluded from transfer generation
if self.is_overdraft():
return result
...
Normal case
Positive reimbursement, negative reward
Negative reimbursement, positive reward
Overdraft
When both slippage_eth and total_cow_reward are in their expected ranges (reimbursement ≥ 0, reward ≥ 0), two separate transfers are generated:# ETH reimbursement → buffer_accounting_target
Transfer(token=None, recipient=self.buffer_accounting_target,
amount_wei=reimbursement_eth)
# COW reward → reward_target
Transfer(token=Token(self.reward_token_address, 18),
recipient=self.reward_target,
amount_wei=total_cow_reward)
The two recipients can differ: buffer_accounting_target receives the slippage reimbursement and reward_target receives the COW reward. When reimbursement_eth > 0 but total_cow_reward < 0 (the solver has a net negative COW reward), the entire net payment is collapsed into a single ETH transfer to avoid a negative COW transfer:if reimbursement_eth > 0 > total_cow_reward:
# Pay total_outgoing_eth() as a single ETH transfer
result.append(
Transfer(
token=None,
recipient=self.buffer_accounting_target,
amount_wei=reimbursement_eth + total_eth_reward,
)
)
return result
Here reimbursement_eth + total_eth_reward == total_outgoing_eth() since there is no slippage offset in this path. When reimbursement_eth < 0 but total_cow_reward > 0 (slippage exceeds native-token outflows but there is still a COW reward to pay), the net is settled as a single COW transfer:if reimbursement_eth < 0 < total_cow_reward:
# reimbursement_cow = reimbursement_eth * total_cow_reward / total_eth_reward
result.append(
Transfer(
token=Token(self.reward_token_address, 18),
recipient=self.reward_target,
amount_wei=reimbursement_cow + total_cow_reward,
)
)
return result
The negative reimbursement is converted to COW at the current ETH/COW exchange rate and netted against the COW reward. When total_outgoing_eth() < 0 (negative slippage exceeds all rewards), the solver is in overdraft. No native-token or COW transfers are generated for the primary reward. Only the quote reward COW transfer (if any) is included:if self.is_overdraft():
return result # result contains only the quote reward transfer
The overdraft is recorded separately — see the Overdrafts page.
Partner fees
Protocol fees collected from trades are split between CoW DAO and registered partner integrators. The fee flow is:
# src/fetch/payouts.py — prepare_payouts()
total_protocol_fee = int(solver_payouts["protocol_fee_eth"].sum())
total_partner_fee = int(partner_payouts["partner_fee_eth"].sum())
# Partner fees are subject to a tax (partner_fee_tax)
total_partner_fee_taxed = sum(
int(row["partner_fee_eth"] * (1 - row["partner_fee_tax"]))
for _, row in partner_payouts.iterrows()
)
total_partner_fee_tax = total_partner_fee - total_partner_fee_taxed
# Remaining protocol fee goes to CoW DAO
net_protocol_fee = total_protocol_fee - total_partner_fee
Three transfer types are created from this:
net_protocol_fee → sent to protocol_fee_safe (0x22af3D38E50ddedeb7C47f36faB321eC3Bb72A76) if positive
total_partner_fee_tax → also sent to protocol_fee_safe (the CoW DAO cut of partner fees)
- Per-partner transfers →
partner_fee_eth * (1 - partner_fee_tax) sent to each partner’s address
for _, row in partner_payouts.iterrows():
partner = row["partner"]
partner_fee = int(row["partner_fee_eth"] * (1 - row["partner_fee_tax"]))
assert partner_fee >= 0
if partner_fee > 0:
transfers.append(
Transfer(token=None, recipient=Address(partner), amount_wei=partner_fee)
)
All partner fee transfers are in the chain’s native token, not COW. The PARTNER_PAYOUTS_COLUMNS list is ["partner", "partner_fee_eth", "partner_fee_tax"].
The RewardAndPenaltyDatum class
This class is the central unit of computation for a single solver’s payout:
class RewardAndPenaltyDatum:
def __init__(
self,
solver: Address,
solver_name: str,
reward_target: Address, # recipient of COW rewards
buffer_accounting_target: Address, # recipient of ETH reimbursements
primary_reward_eth: int,
slippage_eth: int, # combined slippage + network_fee
primary_reward_cow: int,
quote_reward_cow: int, # always >= 0
service_fee: Fraction, # e.g. Fraction(15, 100)
reward_token_address: Address,
):
Instances are constructed from a DataFrame row via RewardAndPenaltyDatum.from_series(frame), which also merges slippage_eth and network_fee_eth into a single slippage_eth field:
@classmethod
def from_series(cls, frame: Series) -> RewardAndPenaltyDatum:
slippage = int(frame["slippage_eth"]) + int(frame["network_fee_eth"])
...