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.
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.
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
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.
Fees
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
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.
Quotes
Token safety
- Seven tabs, thirty-four fields
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
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
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
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
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
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
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.
What each one actually changes:
dexPoolRefreshMsis 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.dexPoolIndexChunkBlocksis the size of onegetLogscall. 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.dexPoolIndexMaxBlocksbounds one pool's sweep, so a pool with a long unindexed history backfills across several passes instead of one enormous request.dexPoolIndexBootstrapBlocksonly 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
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
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.