Skip to content

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 antiDdosFee of feeToken from the trader. A failed transfer skips the intent with skipReason = "FEE" — it is not executed.
  • When antiDdosFee is 0, no transfer is attempted onchain — but the relayer still checks feeToken at submission. A disabled action must be signed with feeToken = 0x0000…0000; pairing a zero fee with a real token address is rejected with 400 anti-ddos not enabled.

Fetching the configuration

http
GET /v2/trading/anti-ddos-config
json
[
  { "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 }
]
FieldMeaning
actionAction number
feeTokenERC-20 address the fee is charged in
antiDdosFeeAmount in that token's decimals, as a string
enabledfalse means this action is free
priorityLower 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.

ts
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 it
ts
export 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:

ActionSpends
MARKET_OPEN, LIMIT_OPENamountIn plus extraFee if you set one
ADD_MARGINamount
ts
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:

StateMeaningUI
config-not-loadedFee config not fetched yetWait
account-state-not-readyBalance/allowance cache not warmWait
needs-approvalBalance is sufficient, allowance is notPrompt for approval
insufficient-balanceNeither is sufficientPrompt 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

SymptomCause
400 antiDdosFee mismatchSigned with a config older than ~120 seconds
400 feeToken not supportedFee token not in the configuration for that action
400 anti-ddos not enabledDisabled action signed with a non-zero fee, or with a feeToken other than 0x0000…0000
skipReason = FEENo approval to the Diamond, or the balance was consumed by the trade itself

Next: Submit & Track.

Trading perpetuals involves risk. Nothing here is financial advice.