Execution Fee
Because the protocol pays gas for 1CT trades, each intent carries a small onchain fee in an ERC-20 token. It is priced in USD, configured per action, and charged when the intent executes.
The fee is part of the signed message (feeToken and antiDdosFee), so you must resolve it before signing — and the values you sign have to match the server's current configuration.
How it works
- Each action has a USD fee amount and a set of accepted fee tokens, each with a priority.
- The server converts USD to token amounts and republishes the configuration every 60 seconds.
- The last two generations of configuration remain valid, so a signature stays acceptable for about 120 seconds after you read the config.
- On execution the contract transfers
antiDdosFeeoffeeTokenfrom the trader. A failed transfer skips the intent withskipReason = "FEE"— it is not executed. - When
antiDdosFeeis0, no transfer is attempted onchain — but the relayer still checksfeeTokenat submission. A disabled action must be signed withfeeToken = 0x0000…0000; pairing a zero fee with a real token address is rejected with400 anti-ddos not enabled.
Fetching the configuration
GET /v2/trading/anti-ddos-config[
{ "action": 0, "actionName": "MARKET_OPEN", "feeToken": "0x7547…", "antiDdosFee": "500000", "enabled": true, "priority": 0 },
{ "action": 0, "actionName": "MARKET_OPEN", "feeToken": "0x3bd3…", "antiDdosFee": "12345678901234567", "enabled": true, "priority": 1 },
{ "action": 1, "actionName": "MARKET_CLOSE", "feeToken": "0x0000000000000000000000000000000000000000", "antiDdosFee": "0", "enabled": false, "priority": 2147483647 }
]| Field | Meaning |
|---|---|
action | Action number |
feeToken | ERC-20 address the fee is charged in |
antiDdosFee | Amount in that token's decimals, as a string |
enabled | false means this action is free |
priority | Lower is preferred |
One action appears on several rows — one per accepted fee token. When an action is disabled, sign with feeToken = 0x0000…0000 and antiDdosFee = 0.
Refresh every 60 seconds and cache the result. Do not fetch it inline while signing.
let feeConfig: AntiDdosConfig[] = []
export async function refreshFeeConfig(baseUrl: string) {
const res = await fetch(`${baseUrl}/v2/trading/anti-ddos-config`)
if (!res.ok) throw new Error(`anti-ddos config: HTTP ${res.status}`)
feeConfig = await res.json()
}
export function startFeeConfigRefresh(baseUrl: string) {
void refreshFeeConfig(baseUrl).catch(console.warn)
return setInterval(() => void refreshFeeConfig(baseUrl).catch(console.warn), 60_000)
}Selecting a fee token
Walk the enabled options for the action in priority order and pick the first one the trader can actually pay.
for config of options.sortBy(priority):
required = config.antiDdosFee
+ (amount this trade spends of the same token, if any)
if balance(feeToken) >= required and allowance(feeToken, diamond) >= required:
use itexport function selectFeeToken(
action: number,
getAccountState: (token: Address) => { balance: bigint; allowance: bigint } | undefined,
additionalSpends: readonly { token: Address; amount: bigint }[] = [],
): { feeToken: Address; antiDdosFee: bigint } {
const options = feeConfig
.filter((c) => c.action === action && c.enabled)
.sort((a, b) => a.priority - b.priority)
if (options.length === 0) return { feeToken: ZERO_ADDRESS, antiDdosFee: 0n }
for (const opt of options) {
const token = opt.feeToken as Address
const state = getAccountState(token)
if (!state) continue
const extra = additionalSpends
.filter((s) => s.token.toLowerCase() === token.toLowerCase())
.reduce((sum, s) => sum + s.amount, 0n)
const required = BigInt(opt.antiDdosFee) + extra
if (state.balance >= required && state.allowance >= required) {
return { feeToken: token, antiDdosFee: BigInt(opt.antiDdosFee) }
}
}
throw new Error('No fee token with sufficient balance and allowance')
}Add the trade's own spend
If the trade itself spends the same token as the fee, that amount must be added to required. Skipping this produces intents that pass selection and then fail onchain with skipReason = "FEE", because the collateral transfer consumed the balance the fee needed.
What counts as the trade's own spend:
| Action | Spends |
|---|---|
MARKET_OPEN, LIMIT_OPEN | amountIn plus extraFee if you set one |
ADD_MARGIN | amount |
const { feeToken, antiDdosFee } = selectFeeToken(
OneClickAction.MARKET_OPEN,
getCachedErc20State,
[{ token: tokenIn, amount: amountIn + extraFee }], // ← this line
)extraFee is the one most integrations miss, because it is optional and usually zero during development. A broker integration that turns it on later will start seeing FEE skips unless this is accounted for.
Cache balances and allowances
selectFeeToken should read from a cache, never issue RPC calls inline — a round trip in the signing path defeats the purpose of 1CT.
Prefetch balanceOf(trader) and allowance(trader, diamond) for every enabled fee token, and refresh:
- on a timer,
- when the window regains focus,
- whenever the fee configuration changes.
Distinguish the failure modes so you can show something useful:
| State | Meaning | UI |
|---|---|---|
config-not-loaded | Fee config not fetched yet | Wait |
account-state-not-ready | Balance/allowance cache not warm | Wait |
needs-approval | Balance is sufficient, allowance is not | Prompt for approval |
insufficient-balance | Neither is sufficient | Prompt to fund |
Approvals
The trader must approve the Diamond for each fee token they might pay in. Without it, intents are accepted and then skipped onchain.
Approving the collateral token is separate — see Authorizing an Agent → Token approvals. When the fee token and the collateral token are the same, the allowance has to cover amountIn + extraFee + antiDdosFee in total.
Failure modes
| Symptom | Cause |
|---|---|
400 antiDdosFee mismatch | Signed with a config older than ~120 seconds |
400 feeToken not supported | Fee token not in the configuration for that action |
400 anti-ddos not enabled | Disabled action signed with a non-zero fee, or with a feeToken other than 0x0000…0000 |
skipReason = FEE | No approval to the Diamond, or the balance was consumed by the trade itself |
Next: Submit & Track.