Settings reference

All 71 Swap settings with their real defaults — the direct-pool risk envelope, the pool indexer, verification freshness, the five Super-Admin keys, and the 37 that have no field on any screen.

19 min readUpdated 6 August 2026settings, reference, direct-pools, venue-policy, super-admin

The addon defines 71 dex* settings. The settings console renders a field for 34 of them. The other 37 are read by the server on every quote, every sweep and every indexer tick, and there is no screen in the product that shows you their value — which is the single most useful thing to know before you go looking for a control that does not exist.

Settings are stored as text. An off switch read straight from the database is the string "false", and "false" is truthy in JavaScript. Every setting here is coerced on read, which is why editing a settings row by hand in a SQL client is not a supported way to change one — see the note at the bottom.

The three doors

Door Carries Who
Admin → Swap → Settings (/admin/dex/settings) 34 fields across seven tabs: General, Fees, Quotes, Token Safety, Compliance, Execution, Liquidity access.dex.settings to reach the screen, view.dex.settings to read, edit.dex.settings to save
Admin → System → Settings → Features → Web3 Trading dexEnabled, and nothing else Super Admin
PUT /api/admin/dex/settings Every key in the list, including the 37 with no field edit.dex.settings, plus the Super Admin role for the five protected keys

The Swap console sends only the keys you changed, so a key with no field is never included in a save and never overwritten by one. It also means a key with no field cannot be set from that screen — the API is the only door.

PUT/api/admin/dex/settingspermission: edit.dex.settings
Writes dex-prefixed keys. Unknown keys are ignored and reported back
curl -X PUT https://your-host/api/admin/dex/settings \
  -H 'Content-Type: application/json' \
  -H 'Cookie: <your admin session>' \
  -d '{"dexDirectPoolMaxTradeToLiquidityBps": 50}'

The response names exactly what happened — updated, unchanged and ignored. A key you spelled wrong lands in ignored rather than being written, so a typo is loud instead of creating a row nothing reads.

Every threshold here is clamped somewhere on read, so a bad row can never take the addon down. That is the wrong behaviour on a save — an operator who types 500 into the fee field and is shown 500 afterwards believes they are earning 5% when they are earning 3%. So the PUT refuses an out-of-range or inconsistent value instead of storing it, and names the one number it disagreed with. See What a save refuses.

The master switches

dexEnabledtype: booleandefault: false
Exposes the swap surface at all. Super Admin only.
dexQuoteOnlytype: booleandefault: false
Quotes render; nothing can be signed. The switch to reach for during an incident.

With this on, the swap button reads Swapping is paused and is disabled, and the ticket explains that the price above is still live. Users can price a trade and cannot execute one, which is what the switch is for.

dexEnabledChainstype: jsondefault: []
Chains offered, as a JSON array of chain ids. Empty means every enabled chain row.

Fees

dexFeeBpstype: numberdefault: 0
Your cut of each swap, in basis points. 30 = 0.30%.
dexMaxFeeBpstype: numberdefault: 100
Ceiling on the above. Capped at 300 — no aggregator honours a fee above 3%.
dexSweepMinValueUsdtype: numberdefault: 50
Minimum USD value before a fee group is swept, so dust does not cost more in gas than it is worth. An UNPRICED accrual counts as ABOVE it. No field on any screen.
dexSweepBatchtype: numberdefault: 200
Pending swaps one confirmation sweep examines.
dexReconcileMaxBlockstype: numberdefault: 50000
Largest block range one revenue reconciliation scan may cover. No field on any screen.

The fee defaults to 0 so a fresh install quotes on every chain. A chain with a fee above zero and no recipient refuses to quote unless you tick Run at zero fee on that chain; see Fees and revenue.

There is no dexFeeRecipient

There was, and it was deleted. The address that receives your fee is the feeRecipient column on the chain row, set in Admin → Swap → Chains, and it has no global fallback — with dexFeeBps above zero, a chain without one refuses to quote.

Two things short-circuit that refusal before the recipient is ever read. Run at zero fee (zeroFeeAcknowledged on the chain row) is checked first, before the fee setting is even looked at — an operator who ticked it has already answered the question, so the chain quotes at zero fee. And a pair routed DIRECT_ONLY never demands a recipient at all, because a raw AMM router has no integrator-fee parameter to carry one; that pair quotes on a chain with nothing set. Anything that reaches an aggregator refuses.

Two reasons the global key is gone, and the first is disqualifying: every settings row bar one is world-readable through the unauthenticated /api/settings, and a settings row is globally singular. One string cannot be both a Gnosis Safe on mainnet and an EOA on Base, and no fallback between those two is ever what you meant. The address is per-deployment state, so it lives beside the RPC override on the chain it belongs to.

If you find a dexFeeRecipient row in your settings table from an older install, nothing reads it. Delete it or leave it; either way, set the recipient per chain.

Slippage and price protection

dexMinSlippageBpstype: numberdefault: 10
Floor a user may set, in basis points.
dexMaxSlippageBpstype: numberdefault: 500
Ceiling a user may set.

The terminal's slippage control offers only the presets inside your band and bounds its custom field to it, and says which band applies when that narrows the choice. Before, a user could pick 10%, the settings screen would keep showing 10%, and the server would quietly clamp the quote to your ceiling — two screens disagreeing about the tolerance on an irreversible trade, with the one the user had just used being the wrong one.

dexDefaultSlippageBpstype: numberdefault: 50
What a user gets before they touch anything.
dexMaxPriceImpactBpstype: numberdefault: 1000
Price impact above which a swap is refused outright. No field on any screen.
dexPriceImpactWarnBpstype: numberdefault: 300
Price impact above which the user is warned but may proceed. No field on any screen.
dexMinLiquidityUsdtype: numberdefault: 25000
Pool liquidity below which a pair may not be bound to a pool. No field on any screen.

Quotes

dexQuoteTtlSecondstype: numberdefault: 60
How long a quote stays valid before the terminal refetches.
dexQuoteTimeoutMstype: numberdefault: 4000
How long to wait for an aggregator before giving up on it.
dexQuoteDedupMstype: numberdefault: 3000
Window in which an identical quote request is served from the last answer.
dexPriceCacheMstype: numberdefault: 5000
Server-side price cache lifetime.
dexQuoteRetentionDaystype: numberdefault: 90
How long quote records are kept before pruning.
dexMaxNotionalUsdtype: numberdefault: 0
Largest single swap, in USD. 0 turns the limit off.

Token safety

  1. Seven tabs, thirty-four fields
dexAllowlistModetype: selectdefault: ALLOWLIST
Which tokens may be swapped. Super Admin only.
dexScreeningEnabledtype: booleandefault: false
Whether tokens are screened at all.
dexScreeningProvidertype: text
Registry name of the screening adapter. Blank marks every token safe without checking it.
dexScreeningMaxAgeDaystype: numberdefault: 30
How old a verdict may be before the token is re-screened.
dexScreeningFailOpentype: booleandefault: true
true = a screening outage does not block trading in already-listed tokens.
dexScreeningSliceSizetype: numberdefault: 50
Tokens re-screened per nightly run. No field on any screen.
dexScreeningWarnTaxBpstype: numberdefault: 500
Buy or sell tax at or above which a token shows a caution chip. 500 = 5%.
dexScreeningBlockTaxBpstype: numberdefault: 2000
Buy or sell tax at or above which a token is REFUSED. 2000 = 20%. Keep it well above the warning.
dexScreeningConcentrationWarnBpstype: numberdefault: 5000
Caution when one unlocked, non-contract holder owns at least this share of supply. 5000 = 50%.
dexScreeningSimulatetype: booleandefault: false
Buy and sell the token inside a simulated block and treat a reverted sell as a honeypot. Needs an eth_simulateV1 RPC.
dexScreeningSimulateSpendMultipletype: numberdefault: 20
Simulation probe size, as a multiple of the chain's own minimum gas reserve.
dexScreeningSimulateTimeoutMstype: numberdefault: 10000
How long the buy/sell simulation may take before it is abandoned. No field on any screen.
dexRequireOnChainDecimalstype: booleandefault: true
Read decimals() from the chain and refuse a row that disagrees. No field on any screen.
dexTokenProbeTimeoutMstype: numberdefault: 6000
How long to wait for a token probe before giving up. No field on any screen.

dexScreeningSimulateTimeoutMs bounds the honeypot round trip, not the vendor call — that one is the provider's own timeout. A simulation that times out contributes nothing rather than failing the token: it can only ever make a verdict worse, never better, so an abandoned probe leaves the vendor's answer standing. Raising it above the default is reasonable on a busy archive endpoint; lowering it below a couple of seconds effectively turns simulation off without turning the switch off, which is the confusing version of the same outcome.

dexScreeningWarnTaxBps shows a chip. dexScreeningBlockTaxBps refuses to quote the token at all. They are 5% and 20% apart on purpose: a 10% sell tax is a real, disclosed design that some tokens use and some operators list deliberately, and setting the refusal threshold down near the warning turns every cautioned token into a delisted one without telling you.

A wrong decimals moves the decimal point on every amount a user ever swaps in that token — it displays a balance a million times too large and asks them to approve a spend a million times larger than the number on screen. It is not recoverable after the signature.

Router safety

dexEnforceRouterAllowlisttype: booleandefault: true
Refuse a quote whose router or approval spender is not on the allowlist.

The router address and the approval spender both arrive in the aggregator's own response and go straight to the user's wallet. This setting is the only thing that reads them before a user signs. Turn it off only if a vendor has rotated a router and quoting has stopped — then add the new address under Chains → Extra routers, which is audited, and turn it back on.

Compliance

dexGeoBlockListtype: jsondefault: []
ISO country codes to refuse, as a JSON array. Empty by default — "we restrict by jurisdiction" is false until you set this.
dexKycRequiredtype: booleandefault: false
Require verified identity before a swap can be built. Super Admin only.
dexRequireWalletLinktype: booleandefault: true
Require a signed message proving wallet ownership before building a swap.
dexRiskWarningEnabledtype: booleandefault: true
Show the risk disclosure the user must acknowledge before their first swap of a session.

With this on, the first time a user presses Swap in a browsing session the ticket shows a disclosure covering the four things that are true of this platform specifically — a swap cannot be reversed, the platform holds neither funds nor keys, the price is a third-party quote that can move, and anyone can create a token. Accepting it continues straight into the swap they asked for.

Once per session, not once per swap. A dialog dismissed forty times an afternoon is one nobody reads, which is a weaker disclosure than none at all — it also creates a record suggesting they read it. The acknowledgement lives in the browser's session storage, so it is gone when the tab closes.

Expert mode does not skip it. Expert mode is a user preference that skips the confirmation step; this is your requirement, and a preference must not be able to switch it off.

The requirement is one free signature, made from the Swap ticket itself (the primary button reads Link wallet) or from Profile → Wallet. It moves no funds, costs no gas or energy, and grants no permissions.

What the user signs depends on the chain: SIWE on EVM, a short plain-text statement on Solana and TRON, and a ton_proof on TON — which TON Connect issues as part of a connection, so a TON user reconnects once when they link. See Troubleshooting for the per-network detail and the one TRON wallet requirement.

Turning it off is still a legitimate choice — it simply stops the platform asking users to prove the address they swap from.

Execution and market data

dexDropAfterMstype: numberdefault: 1800000
How long a submitted transaction may go unseen before it is treated as dropped.
dexSymbolIdleMstype: numberdefault: 300000
Idle time before a market symbol's poller is reaped. Floored at 30s.
dexMarketProvidertype: selectdefault: geckoterminal
Primary market-data provider. No field on any screen.
dexMarketProviderFallbacktype: select
Fallback provider when the primary fails. "" means no fallback. No field on any screen.
dexChartCacheTtlSectype: numberdefault: 86400
How long chart candles are cached. No field on any screen.
dexTickerPollMstype: numberdefault: 10000
Ticker refresh interval. Floored at 2s. No field on any screen.
dexOhlcvPollMstype: numberdefault: 15000
Candle refresh interval. Floored at 2s. No field on any screen.
dexTradesPollMstype: numberdefault: 8000
Trade-feed refresh interval. Floored at 2s. No field on any screen.
dexMarketRefreshMstype: numberdefault: 60000
Market statistics sweep interval. No field on any screen.

The direct-pool half

Everything below governs the venue where your own money is the liquidity. It exists as a separate set of keys rather than a retune of the ones above for one reason: the global values govern pools you did not choose and do not profit from, and these govern a pool you chose, sized and earn the LP fee on. They must be able to differ without weakening the aggregator path.

None of the keys in this half has a field on any screen except dexDirectPoolsEnabled and dexPoolRiskAckRequired, which live on the console's Liquidity tab. The rest are set through the API.

The routing decision — dexVenuePolicy

dexVenuePolicytype: selectdefault: DIRECT_ONLY_WHEN_UNQUOTED
Which venue may fill a swap, and who wins when both can.

Four values are legal globally:

Value Behaviour
AGGREGATOR_ONLY A direct pool is never even constructed as a candidate.
DIRECT_ONLY_WHEN_UNQUOTED The default. The direct pool is reached only when no aggregator quoted at all. An aggregator that answered wins outright — no comparison, no margin.
AGGREGATOR_PREFERRED Both race. The aggregator wins any tie and any margin below the threshold.
BEST_EXECUTION Both race. The net-output-plus-margin rule decides.

The default is the most conservative policy that still delivers the point of the feature — pairs no aggregator will quote become tradable — while changing nothing for the majors. Turning direct pools on does not re-route WETH/USDC.

DIRECT_ONLY is a per-pair value and has no global form

There is a fifth value, and the settings PUT refuses it by name. Per pair, DIRECT_ONLY is a legitimate statement: this is my token, my pool, do not bother asking 0x. Globally it routes every pair — including the majors — through whatever pool happens to be bound, at a fill no user can see the cause of. There is no honest use for the global form, so it does not exist there.

Set it per pair instead: Admin → Swap → Listings → Pairs, the Routing column. Each pair holds INHERIT (the default, meaning "use the global value"), any of the four above, or DIRECT_ONLY. A pair set to DIRECT_ONLY with no bound pool is rejected by the model itself, because every quote on it would refuse with NO_ROUTE.

An unrecognised policy on a pair falls back to your global value rather than to a hardcoded default — your platform-wide choice is a better guess at your intent than anything the code could invent.

The direct-pool risk envelope

dexDirectPoolsEnabledtype: booleandefault: false
Whether a swap may be filled from a pool the platform runs. The fifth kill-switch layer. Super Admin only.
dexDirectPoolV3Enabledtype: booleandefault: true
Whether V3 pools may quote. Turning it off drops the QuoterV2 dependency without disabling the venue class.
dexDirectPoolWinMarginBpstype: numberdefault: 50
How much better a direct route must be, in bps, before it displaces an aggregator.
dexDirectPoolMaxImpactBpstype: numberdefault: 100
Price-impact ceiling on a direct route. Stricter than dexMaxPriceImpactBps, and must stay at or below it.
dexDirectPoolMinLiquidityUsdtype: numberdefault: 250000
Depth floor for a direct route. An order of magnitude above dexMinLiquidityUsd.
dexDirectPoolMaxTradeToLiquidityBpstype: numberdefault: 100
One swap may not exceed this share of on-chain reserves. 100 = 1%.
dexDirectPoolMaxSlippageBpstype: numberdefault: 100
Slippage ceiling on a direct route. TIGHTER than dexMaxSlippageBps on purpose.
dexDirectPoolDefaultSlippageBpstype: numberdefault: 100
Reserved. Intended as the starting slippage for a direct quote; validated on save, but no quote path reads it — see below.
dexDirectPoolOperatorShareWarnBpstype: numberdefault: 5000
Share of pool liquidity above which the console tells you that you ARE the market.
dexDirectPoolQuoteTimeoutMstype: numberdefault: 2500
Ceiling on the QuoterV2 eth_call for a V3 pool.
dexDirectPoolPrivateRpcRequiredtype: booleandefault: false
Reserved. Nothing in the product reads it today — see below.

dexDirectPoolsEnabled is layer five of the kill-switch ladder and the first thing every quote checks — before any model query, before any RPC spend, so turning it off costs one boolean read. It also gates the pool indexer at boot. Off, a direct pool is not priced, not indexed, and a pair set to DIRECT_ONLY becomes unquotable by anything. Swaps already broadcast are still watched and settled either way; switching the venue off never strands a transaction.

The four gates, in the order they refuse

A direct candidate must clear all four before it can enter the race, and each refusal names both the threshold and what was observed:

Gate Reads Refusal
Quotable Pool state is ACTIVE and its verification is fresh POOL_NOT_ALLOWLISTED
Depth dexDirectPoolMinLiquidityUsd POOL_TOO_SHALLOW
Trade-to-liquidity dexDirectPoolMaxTradeToLiquidityBps POOL_TOO_SHALLOW
Price impact dexDirectPoolMaxImpactBps DIRECT_POOL_IMPACT_CEILING

The trade-to-liquidity cap is the load-bearing one, and it is not a duplicate of the impact ceiling. On a constant-product pool, impact is roughly trade size over reserves for small trades, so a 1% cap is about 100 bps of impact by construction — a structural bound that still holds when no impact figure can be computed at all. A swap at 2% of reserves has around 200 bps of impact, which is under a 300 bps warning and over the 1% ratio: refused here and nowhere else.

Two nulls are refusals rather than passes, and both directions matter:

  • A pool whose reserves cannot be priced in USD refuses with POOL_TOO_SHALLOW. That is exactly the pool whose depth cannot be vouched for, so letting a null through would make the floor a no-op on precisely the tokens it was written to catch.
  • A null price impact refuses with DIRECT_POOL_IMPACT_UNCOMPUTABLE. On an aggregator route an unreported impact is only a warning — a vendor not reporting a number is not evidence of anything. Here the platform holds the reserves itself, so a null is not "unreported", it is "degenerate".

The win margin prices a conflict of interest

netOut(aggregator) is net of your own integrator fee. netOut(direct) is net of nothing, because a raw AMM router has no fee hook to net — see Why a direct pool earns no swap fee. At a zero margin a direct pool wins any time it is within roughly your fee of the aggregator, and the platform routes away from its own revenue on a rounding difference, invisibly, on every trade.

Comparing gross output would fix that by routing users to a worse fill, which is the opposite of best execution. So the comparison is on net output — what the user actually receives — and a material margin is required before displacing a venue with third-party competition, MEV protection and a vendor SLA behind it.

Two details you will see in the quote log:

  • The margin doubles when either side's net output could not be computed and the comparison drops to gross, because a gross comparison systematically flatters the venue with no fee — always the direct one.
  • Exactly at the margin, the aggregator wins. The comparison is strict: the burden of proof sits on the venue displacing the incumbent.

Zero is accepted as a deliberate choice (best net output wins outright) and is not recommended, for the reason above. It is documented, not forbidden.

Slippage on a direct route

The effective ceiling is the lower of dexMaxSlippageBps and dexDirectPoolMaxSlippageBps, so setting the direct ceiling above the global one is not an error — it simply never binds.

The direction of the ceiling is the point: on a thin pool a wide tolerance is a standing, publicly visible invitation to sandwich for exactly that amount, because the attacker reads amountOutMin straight out of the mempool calldata. That is why the direct ceiling is 100 bps against the global 500.

There is no separate starting value on a direct route. Every quote starts from the slippage the user asked for, or from dexDefaultSlippageBps (50) when they asked for nothing, and a direct fill is then re-clamped down to the effective ceiling above. dexDirectPoolDefaultSlippageBps is defined, defaulted to 100 and refused on save when it sits above that ceiling — but no quote path reads it, so raising it widens nothing.

dexDirectPoolPrivateRpcRequired is inert

It is defined, defaulted, validated on save and read by nothing. There is no warning today when a direct-pool transaction is built against a public endpoint. Setting it changes no behaviour; treat the private-RPC decision as an infrastructure choice you make in Environment, not as something this switch enforces.

Pool verification freshness

dexPoolVerifyMaxAgeHourstype: numberdefault: 48
How old a pool's factory verification may be and still be quotable.
dexPoolReverifyHourstype: numberdefault: 24
Reserved. Intended as the re-verification interval; nothing reads it today.

Quoting a pool requires two independent facts, and they are deliberately not merged into one status: the lifecycle (state is ACTIVE — how far through create → seed → retire the pool is) and the trust axis (verifiedAt is fresher than dexPoolVerifyMaxAgeHours).

A stale verification stops the pool being quotable. It is not a warning badge. The refusal is POOL_VERIFICATION_STALE, and the pool goes on looking perfectly healthy on its own row — ACTIVE, seeded, with reserves — while every quote against it refuses. The same freshness check drives the poolVerified line on the pool readiness panel, so that panel is where you see it first.

The reason is that a verification is a claim about a moment. A factory can be superseded, a token row edited, a deployment removed from the registry. Treating a two-month-old proof as current is how a pool stays quotable after the fact that made it quotable stopped being true.

dexPoolReverifyHours is defined and defaulted to 24 hours and no code reads it. There is no re-verification sweep — the stale message says the pool "will re-verify on the next sweep", and that sweep does not exist yet.

In practice: a pool that has not been verified within dexPoolVerifyMaxAgeHours stops quoting until somebody presses Verify on Admin → Swap → Liquidity → Pools. With the 48-hour default that is a manual task every other day for every active pool. Raise dexPoolVerifyMaxAgeHours if that cadence does not match how often anyone opens the screen — but understand what you are extending, and see Direct pools for what verification actually checks.

The pool indexer

The indexer turns a pool's own Swap logs into 1-minute candles, so a market whose chart comes from your pool has one. It runs on a single process behind a lease, and it does not arm at all while dexDirectPoolsEnabled is off.

dexPoolRefreshMstype: numberdefault: 300000
Indexer tick interval, and the window each tick re-derives. Floored at 30s by the indexer itself.
dexPoolIndexChunkBlockstype: numberdefault: 1000
getLogs span per request. Many RPCs cap this at 1000 blocks.
dexPoolIndexMaxBlockstype: numberdefault: 50000
Ceiling on blocks covered per pool per sweep, so a dark pool backfills over several passes.
dexPoolIndexBootstrapBlockstype: numberdefault: 200000
How far back to start when the pool's creation block is unknown.
dexPoolIndexReorgRewindBlockstype: numberdefault: 50
How far to rewind when the cursor block's hash has changed.

What each one actually changes:

  • dexPoolRefreshMs is two things at once — the interval the indexer timer fires at, and the length of the window each tick re-derives (plus a fixed five-minute overlap, so a log arriving at the edge of the previous pass still lands in its bucket). Lowering it makes charts fresher and multiplies your RPC bill; the indexer floors it at 30 seconds whatever you set.
  • dexPoolIndexChunkBlocks is the size of one getLogs call. This is the one to lower when your provider returns "block range too large" — most public endpoints cap at 1000, some at 500. A chunk that fails is skipped rather than retried in a loop, and the next pass covers the hole.
  • dexPoolIndexMaxBlocks bounds one pool's sweep, so a pool with a long unindexed history backfills across several passes instead of one enormous request.
  • dexPoolIndexBootstrapBlocks only applies when the pool's creation block is not known. It is the floor the backfill starts from, counted back from the confirmed head. Too small and early history is silently missing; too large and a first pass walks blocks that contain nothing.

The reorg rewind is destructive, and deliberately so

The indexer never reads past head − requiredConfirmations, so a reorg shallower than the confirmation depth cannot touch an emitted candle — those blocks were never read. A deeper reorg is detected by re-reading the block the cursor sits on and comparing its hash.

When the hash has changed, the indexer rewinds dexPoolIndexReorgRewindBlocks blocks, deletes the recorded pool events above the rewind point, and re-derives the affected candles from scratch. It does not patch a bar in place: a partially corrected bar is indistinguishable from a correct one afterwards, so there would be no way to notice the difference later. The rewind is logged as a warning naming the pool and the block.

An unreadable block is not treated as a reorg. Treating an RPC hiccup as one would rewind and re-derive on every transient failure, which is an RPC bill and a churned cache for nothing.

Seeding your own pool

dexSeedSlippageBpstype: numberdefault: 100
The amountAMin/amountBMin tolerance on your own add-liquidity transaction. IGNORED on a first add.
dexSeedMaxDeviationBpstype: numberdefault: 500
How far the implied seed price may deviate from an independent reference price before the plan refuses.

dexSeedSlippageBps is not swap slippage. It bounds how far the pool may have moved between the moment the console rendered your calldata and the moment your transaction mines. On a subsequent add the router recomputes the optimal ratio from live reserves and refunds the excess, so the minimums are real protection.

On a first add they are structurally meaningless. Uniswap V2 takes the reserveA == 0 && reserveB == 0 branch: nothing can revert, because there is no reference price to compare against, and the ratio you supply is the price. The console reports seedSlippageBps as null on a first add for exactly that reason, and the real protection there is the implied price rendered in both directions plus a typed confirmation of it — see Direct pools.

dexSeedMaxDeviationBps is the other half of that protection, and it only applies when both tokens have an independent price. Above it the plan refuses; the refusal is overridable, but only with a deliberate flag that costs a second confirmation in the console.

The LP-fee sweep floor

dexLpFeeSweepMinValueUsdtype: numberdefault: 100
The LP-fee equivalent of dexSweepMinValueUsd. Nothing reads it today.

dexSweepMinValueUsd is real: the settlement cron leaves a swap-fee group unswept below it, so dust does not cost more in gas than it is worth, and an unpriced accrual counts as above the threshold rather than below — withholding a fee merely because it could not be priced would strand it indefinitely, which is the opposite of what a minimum is for.

dexLpFeeSweepMinValueUsd is its intended twin and is read by nothing. There is no LP-fee sweep to floor. On a V2 pool there structurally cannot be one: a burn returns principal and fees in one indistinguishable pair of transfers, and the only expressible number is fees minus impermanent loss, which is neither. On V3 the arithmetic exists, and the withdraw path still writes zero profit rows today. Setting this key changes no behaviour — see LP positions.


The five Super-Admin-only settings

dexEnabled, dexAllowlistMode, dexKycRequired, dexDirectPoolsEnabled and dexPoolRiskAckRequired require the Super Admin role — not a permission. edit.dex.settings is not enough, and the server re-resolves the role on every request rather than trusting anything the screen asserts.

Key Where it is edited Rendering for a non-Super-Admin
dexEnabled System → Settings → Features → Web3 Trading Read-only tile for everyone, Super Admin included
dexAllowlistMode Swap → Settings → Token Safety — the only screen Read-only tile
dexKycRequired Swap → Settings → Compliance — the only screen Read-only tile
dexDirectPoolsEnabled Swap → Settings → Liquidity — the only screen Read-only tile, no link, because there is nowhere to send you
dexPoolRiskAckRequired Swap → Settings → Liquidity — the only screen Read-only tile, no link

A Super Admin gets a live control on the Swap console for four of the five, with a line underneath saying the role is why. The tile also names the role rather than the permission, because "why can I change this and my colleague cannot" is the next question and the permission that opened the screen is not the answer.

dexAllowlistMode and dexKycRequired also had fields on System → Settings → Features, and the platform settings endpoint validates nothing beyond a key's shape and a value's length — so the same row had two editors and only this console checked that the value meant anything. That endpoint now answers 403 for both keys, naming this screen. dexEnabled stays there and cannot move: this console answers 503 while it is off, so it can never be the screen that turns it on, which is also why it renders read-only here for everyone.

dexEnabled stays read-only even for a Super Admin, and the reason is structural rather than a policy choice: every page under Swap — including the settings console itself — stops answering while the master switch is off, so this screen can never be the place it is turned on. Offering an off-switch whose only effect is to close the console you are standing in is a trap, not a control. It lives in platform settings so there is always a screen to turn it back on from.

dexFeeBps is deliberately not in that set: an operator changing their own revenue share harms nobody but themselves. It is audited instead, and the audit trail carries the old and new value for that key specifically.

If a save includes a protected key and the caller does not hold the role, the entire request is refused before anything is written — nothing is half-applied, and the message names the keys it objected to.

What a save refuses

The PUT applies one patch atomically and reports one violation, not a list: a patch is all-or-nothing, so the second violation is unreachable information until the first is fixed.

Refusal Fires when
Blank number A numeric field was cleared. "" coerces to 0 through Number(), which is how a cleared field silently becomes zero
Below floor Any numeric key under its minimum — most are 1, a few are 0, dexPoolRefreshMs is 1000
Fee above ceiling dexMaxFeeBps above 300 bps, or dexFeeBps above dexMaxFeeBps
Slippage band The floor above the ceiling, or the default outside the band
Impact inversion dexPriceImpactWarnBps at or above dexMaxPriceImpactBps — at or above it the warning can never fire, because the quote is refused first
Direct slippage A direct default above the effective ceiling, or an effective direct ceiling below the global floor
Direct impact dexDirectPoolMaxImpactBps above dexMaxPriceImpactBps — above it the direct ceiling can never bite, so a thin pool would be governed by the loose threshold
Venue policy Anything outside the four global values, with DIRECT_ONLY refused by name and told where it belongs
Curation policy Anything outside ALLOWLIST, ALLOWLIST_PLUS_WARNED, OPEN
Unknown chain id An id in dexEnabledChains the registry does not have — the list is an allowlist, so naming an unknown id would disable every chain except ones that do not exist
Unknown screener A dexScreeningProvider this build does not ship

That last one is worth its own sentence. A provider name outside A–Z a–z 0–9 _ . - is nulled on read, and a null name resolves to the noop screener — which marks every token safe without checking it, with no critical log, on an install whose operator believes screening is on. A slug with a space in it is therefore worse than a typo, and this is the only place a human is present to be told.

The running platform holds its own copy of every setting behind a cache that does not expire, so a hand-edited row reaches neither layer — the platform goes on serving the old value, and restarting reloads the same stale copy rather than your edit. Every change you make appears to be ignored, which sends you looking for a second fault that does not exist. Change settings through the admin screens, or through PUT /api/admin/dex/settings, which flushes the cache when it writes.