Skip to content

Charge a 20 bps fee on swaps - #700

Merged
n13 merged 3 commits into
mainfrom
n13/swap-app-fee
Oct 13, 2026
Merged

n13 merged 3 commits into
mainfrom
n13/swap-app-fee

Conversation

@n13

@n13 n13 commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Swaps earned Quantus nothing: the quote request sent no appFees. The only fee was 1Click's own 20 bps, which goes to 1Click.

Changes

  • Our fee: every 1Click quote now sends appFees: [{recipient: <our NEAR account>, fee: 20}]. The values are AppConstants.oneClickFeeBps and AppConstants.oneClickFeeRecipient, next to the referral. The recipient is a temporary implicit account for now.
  • What users pay: QTC swaps use confidential (basic) quotes, so 1Click keeps its 20 bps and adds ours on top rather than splitting it. A swap costs 40 bps (0.40%) in fees, and half goes to us. Dry quotes made during the research confirm this:
    • 1Click echoes [{ours, 10}, {1Click, 20}] for a 10 bps request.
    • The output drops by exactly the added basis points.
  • Echo check: 1Click appends its own entry to the echoed appFees, and the quote signature doesn't cover them. A quote is now refused unless the echoed list contains our entry exactly as sent.

Tests

  • swap_service_test:
    • the request body carries our fee
    • fixtures echo our fee plus 1Click's, as the live API does
    • a quote whose echo leaves our fee out is refused
  • swap_flow_test: the fake 1Click echoes both fees.
  • melos run analyze is clean.

Before relying on it

Fees from confidential swaps may settle in a confidential balance on NEAR Intents, which needs a signed session to read or move. One small real swap should confirm where the fee lands and that it can be withdrawn.

Every 1Click quote now carries appFees with our 20 bps for
liq.selfevident.near. On the confidential quotes QTC swaps use, 1Click keeps
its own 20 bps on top, so a swap costs 40 bps in fees, half of them ours.
1Click appends its own entry to the echoed appFees and the quote signature
does not cover them, so a quote is refused unless the echo holds our fee as
sent.
@n13 n13 added bot-review Request automated review from review-bot and removed bot-review Request automated review from review-bot labels Oct 13, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer model: Opus 5.5

Reviewed at 06e8472. The change does what it says. Every quote sends our fee, and a quote whose echo drops it is refused. swap_service_test (46 tests) and swap_flow_test (30 tests) pass locally. I found no correctness bug. The points below are about robustness, docs and disclosure.

  1. The fee is hard-coded and the check fails closed, so a change on 1Click's side stops all swaps until a release. quantus_sdk/lib/src/services/swap_service.dart:69 and :243, quantus_sdk/lib/src/constants/app_constants.dart:53-54.

    • The check needs the echo to hold exactly {liq.selfevident.near, 20}. appFees is unsigned, and 1Click already rewrites it on some paths. Keyless public dry quotes made during this review echoed our 20 as 10 and our 40 as 20, with 1Click taking a cut.
    • Your probes show the confidential path with the partner key echoes it unchanged. But that rests on 1Click's partner terms, which we don't control. If they change, every dry and live quote throws SwapQuoteIntegrityException. The only remote lever is enableSwap, which turns swaps off entirely.
    • The same applies if the recipient ever has to change. Every installed build keeps paying the old account until users update.
    • quantusAssetId already comes from remote config "so a surprising listing needs no update". I suggest passing the fee bps and recipient into SwapService the same way, with a hard upper bound in code. Then a bad echo or a recipient change is a config change, not a release.
  2. mobile-app/docs/near-intents-swaps.md is now wrong.

    • Line 119 lists what POST /v0/quote sends and which echo fields are checked. Neither mentions appFees.
    • The Fees section (154-158) says 1Click echoes its fee "on a request that sent none" and treats 1Click's fee as the only one.
    • Lines 169, 192 and 220 give the user's cost and the preflight cost without our 20 bps.

    Please update it in this PR.

  3. Users are not told about the new fee. The review screen's only fee row is the QTC network fee (mobile-app/lib/v2/screens/swap/review_swap_screen.dart:204). Our 0.2% and 1Click's 0.2% are hidden in the rate. The form's estimate (swapEstimateOut, mobile-app/lib/v2/screens/swap/swap_providers.dart:85) uses listed prices with no fees. So the gap between form and review grows from about 0.2% to about 0.4%, with nothing on screen to explain it. This is a product call, but a wallet that takes its own cut usually shows it.

  4. Test gaps in quantus_sdk/test/services/swap_service_test.dart.

    • The only negative test (line 317) drops our recipient. Both fees are 20, so a predicate that checks only the recipient still passes the suite. Add a case with our recipient and a different fee.
    • No test covers a missing appFees. In that case swap_service.dart:242 throws a raw TypeError, not SwapQuoteIntegrityException. It still fails closed and matches the other casts in this method, so this part is minor.
    • _quoteResponse (line 74) always adds appFees back after ...?request, so it cannot express a missing echo. A test built on it would pass for the wrong reason.
  5. Small test cleanups.

    • _ourFee (line 23) rebuilds _appFee from the same constants, and line 178 then pins it to a literal. Writing the literal once in the body expectation does the same job.
    • The 1Click fee recipient hex is copied from swap_service_test.dart:22 into mobile-app/test/screens/swap_flow_test.dart:179.

On "Before relying on it": the withdrawal looks solvable.

  • On mainnet, intents.near has auth-by-predecessor enabled for liq.selfevident.near. The account can call ft_withdraw or mt_withdraw directly, and add_public_key to get a key for signed intents.
  • It has no keys and no balance there yet.
  • One real swap is still needed to see whether the fee lands in the public or the confidential balance.

Verdict (advisory): Approve. None of these is a correctness bug. I would still do 1 and 2 before this reaches users.

@n13 n13 removed the bot-review Request automated review from review-bot label Oct 13, 2026

@n13 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE (opinion) — no blocking findings at 06e84723f411103968c017b0e6302d40fd6f6d93 (base 572e6a5ea76ea9fbb4785debac3c28dcffe5a745).

Reviewed the complete four-file diff and traced preview/live quotes, exact-input/exact-output handling, response verification, and swap confirmation. Both getQuote and createSwap use _quoteJson, so the fee and echo check cover both paths. The predicate checks both our recipient and amount while allowing 1Click's additional entry. Quoted amounts continue to drive the displayed terms and deposit.

Nonblocking: update mobile-app/docs/near-intents-swaps.md:119 and its Fees section at line 152 to include the Quantus fee and echo check; the existing text still describes requests without our fee.

Validation:

  • dart run melos exec --concurrency=1 --scope=quantus_sdk -- 'flutter test --no-pub --reporter=expanded test/services/swap_service_test.dart': 46 passed.
  • dart run melos exec --concurrency=1 --scope=resonance_network_wallet -- 'flutter test --no-pub --reporter=expanded test/screens/swap_flow_test.dart': 30 passed.
  • Workspace dart format lib test --line-length=120 --output=none --set-exit-if-changed through Melos: 817 files checked, zero changes.
  • git diff --check: passed. GitHub Analyze and dependency checks: successful at the reviewed head.
  • Local workspace analysis was terminated at the required 10-second cap; cold-wallet and miner completed cleanly, SDK/mobile analysis remained incomplete.

Live limitation: a keyless confidential dry-quote probe returned HTTP 401, so I could not independently confirm the configured partner's fee policy. 1Click's fee documentation describes a generic split with partner-specific exceptions. The 20+20 confidential behavior reported in this PR should remain distinguished from that generic policy. Fee settlement and withdrawal remain unverified, as the PR already notes; this review did not fund a swap.

Posted as an opinion because the authenticated reviewer account authored this PR.

@n13 n13 changed the title Charge a 20 bps fee on swaps, paid to liq.selfevident.near Charge a 20 bps fee on swaps Oct 13, 2026
@n13
n13 merged commit ebfa0cd into main Oct 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant