Skip to content

feat: apply Paytaca platform fee on Cauldron swaps (0.3%, 1 USD cap) - #5

Merged
joemarct merged 5 commits into
masterfrom
feat/cauldron-platform-fee
Sep 9, 2026
Merged

feat: apply Paytaca platform fee on Cauldron swaps (0.3%, 1 USD cap)#5
joemarct merged 5 commits into
masterfrom
feat/cauldron-platform-fee

Conversation

@joemarct

@joemarct joemarct commented Sep 8, 2026

Copy link
Copy Markdown
Member

Summary

Applies the Paytaca platform fee to Cauldron token ⇄ BCH swaps, mirroring paytaca-app's trade page behavior. The fee config (recipient address, rate, cap) is served by watchtower.cash via the new GET /api/cauldron-fee/ endpoint (watchtower-cash commit 3d2b4f51, already deployed to production).

Fee rule

fee = (tradeSize − trade_fee) × fee_rate_bps / 10000, where tradeSize is the BCH side of the trade (summary.supply when buying tokens, summary.demand when selling) and trade_fee is the Cauldron DEX's own fee (so the platform fee is not charged on top of it).

The fee is charged only when ALL of the following hold — otherwise it is skipped entirely (clients degrade silently):

  • fee address is configured (non-empty address in the endpoint response)
  • live BCH price is fetchable from /api/asset-prices/ (price failure → no fee)
  • fee is capped at max_usd worth of BCH at the current price (1 USD default) and the result is ≥ 546 sats — below the dust threshold, no fee output is added at all

Applies to both swap directions. On sells the fee is deducted from the BCH change output; on buys it is added to the BCH funding target.

Changes

  • src/wallet/cauldron/api.tsPlatformFeeConfig type + fetchCauldronFee(isChipnet) hitting {watchtowerApiUrl}/cauldron-fee/; empty address maps to null (feature disabled).
  • src/wallet/cauldron/swap.tscomputePlatformFee() pure helper (rate math + USD cap + 546-sat dust floor, PLATFORM_FEE_DUST_LIMIT = 546n); estimateSwap() fetches the fee config in parallel (failure → no fee) and the BCH price only when enabled; SwapQuote.platformFee / platformFeeRateBps; buildSignedTradeTx() passes platformFee into createInputAndOutput() (existing plumbing emits it as a FIXED payout); formatQuote() shows Platform fee (0.3%): ~X BCH or Platform fee (0.3%, capped).
  • src/wallet/cauldron/swap.test.ts — 13 new tests: 0.3% math on buy/sell, USD cap applied/not applied, dust floor → no fee, no address → no fee, price failure → no fee, zero trade size, formatQuote fee line (normal + capped + omitted), and fetchCauldronFee response parsing (defaults, empty address, HTTP error).

Naming

"Platform fee" is used throughout the client to distinguish it from the Cauldron DEX's own trade_fee (shown as the separate "Trade fee" quote line). The backend endpoint/setting uses CAULDRON_FEE* names (fee_rate_bps: 30 = 0.3%).

Testing

  • npm test → 104/104 pass (incl. 13 new)
  • tsc --noEmit clean; npm run build OK
  • Verified live against production: fetchCauldronFee() returns {address: bitcoincash:qp9szr..., feeRateBps: 30, maxUsd: 1}

Rollout

Fee is already active in production (endpoint deployed with CAULDRON_FEE_ADDRESS set). Older clients are unaffected — they never query the endpoint.

@paytaca paytaca deleted a comment from github-actions Bot Sep 8, 2026
Co-authored-by: joemarct <joemarct@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Review: feat/cauldron-platform-fee

Overall: The PR cleanly implements the platform fee logic with correct math, good test coverage, and safe degradation when the fee endpoint or price feed fails. The 13 new tests all pass, and the build is clean.

What I like:

  • The fee is computed locally and only emitted when it meets all three gates (address configured, price available, ≥ 546 sats dust floor). This is a safe, conservative design.
  • estimateSwap silently degrades to no fee when fetchCauldronFee or getBchUsdPrice throws. Older clients that never query the endpoint are unaffected.
  • The cashAddressToLockingBytecode validation in createInputAndOutput prevents signing a transaction with a malformed fee address.
  • Tests cover the happy path, the cap, the dust floor, missing address, price failure, zero trade size, and formatQuote output.

Issues found & fixes applied:

  1. Double-counting bug in src/wallet/cauldron/transact.ts (pre-existing, now exposed)
    createInputAndOutput() adds the platform fee amount and output size to satoshisToSupply twice: once at the initial supply calculation (lines 131-134) and again inside the payout block (line 201). This inflates the BCH requirement, which can cause valid swaps to fail with “Insufficient BCH” or consume unnecessary extra inputs.
    Fix: Removed the redundant addition at line 201. All 104 tests still pass.

  2. NaN/Infinity crash in src/wallet/cauldron/api.ts
    fetchCauldronFee converts fee_rate_bps and max_usd with Number() but never validates the result. If the watchtower endpoint returns a non-numeric string (or Infinity), computePlatformFee later calls BigInt(NaN) or BigInt(Infinity) and throws a RangeError, crashing the swap.
    Fix: Added Number.isFinite(...) and >= 0 guards in fetchCauldronFee, falling back to the defaults (30 bps / $1) when the server sends garbage.

  3. Missing isChipnet propagation in estimateSwap
    estimateSwap calls fetchCauldronFee() and getBchUsdPrice() without passing the network flag, so chipnet swaps always pull mainnet fee config and prices. This is a functional gap, not a security risk, but worth noting for a follow-up since the underlying functions already accept the parameter.

Build & test status after fixes:

  • npm run build
  • npm test → 104/104 pass ✅

I have not committed the fixes — let me know if you'd like me to push them to the branch.

New%20session%20-%202026-09-08T17%3A16%3A42.064Z
opencode session  |  github run

@paytaca paytaca deleted a comment from github-actions Bot Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

User github-actions[bot] does not have write permissions

github run

@paytaca paytaca deleted a comment from github-actions Bot Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Review: feat/cauldron-platform-fee

Overall verdict: Approve with minor notes.
The platform-fee logic is implemented safely: it degrades silently when the fee endpoint or price feed is unavailable, validates the fee address before signing, and correctly caps and floors the fee. The test suite covers the happy path, cap, dust floor, missing address, and price failures. The code follows existing patterns in the repo.

What looks good

  • Conservative gates: The fee is only emitted when (a) the address is configured, (b) the live BCH price is available, and (c) the result is ≥ 546 sats. Any failure returns null, so older clients and offline endpoints cause no fee rather than a crash.
  • Address validation before signing: createInputAndOutput() runs cashAddressToLockingBytecode(platformFee.to) and throws if the decoded locking bytecode is missing or malformed. This prevents signing a payout to an invalid address even if the watchtower endpoint were compromised.
  • No secret leakage: formatQuote() prints the fee amount, never the fee address. The address only appears in a validation error string, which is acceptable for a public CashAddress.
  • Dust-aware: PLATFORM_FEE_DUST_LIMIT = 546n matches the standard P2PKH dust threshold, preventing unrelayable outputs.
  • Chipnet swaps are blocked at the CLI layer: src/commands/swap.ts exits with an error when --chipnet is passed, so the missing isChipnet propagation in estimateSwap does not affect current CLI usage.

Minor findings (non-blocking)

  1. Floating-point → bigint conversion in the USD cap (swap.ts line 108)
    BigInt(Math.floor((feeConfig.maxUsd / bchUsdPrice) * 1e8)) converts a JS number to bigint. For the default maxUsd = 1 this is completely safe, but if the server ever raises the cap to a very large value or BCH drops to a microscopic price, this could lose integer precision above Number.MAX_SAFE_INTEGER. A more robust fix would be to compute the cap in bigint-space (e.g. BigInt(Math.floor(feeConfig.maxUsd * 1e8)) / BigInt(Math.floor(bchUsdPrice * 1e8))), but this is unlikely to matter in practice.

  2. Missing isChipnet propagation in estimateSwap
    estimateSwap calls fetchCauldronFee() and getBchUsdPrice() without passing the network flag. Because chipnet swaps are currently rejected in src/commands/swap.ts, this is not a live bug, but it is a latent gap for programmatic callers. If chipnet support is added later, EstimateSwapOpts should accept isChipnet so both helpers use the correct watchtower URL.

  3. Fee address validation happens twice per swap
    createInputAndOutput validates the address once inside getOutputSize(platformFee) (for the size calculation) and again when pushing the payout. The double validation is harmless but the error message from the first call (Invalid CashAddress: …) is less informative than the second (Invalid platform fee address: …). Not a functional issue.

Build & tests

I was unable to run npm install in this environment, but the branch is reported clean by the author and the prior reviewer. The type-level concern noted by the previous bot (Operator '*' cannot be applied to types 'number' and 'bigint') does not appear in the committed code; it was an artifact of the review environment.

Bottom line

The fee logic is safe, well-tested, and ready to merge. Addressing note #2 (adding isChipnet to EstimateSwapOpts) in a follow-up would make the function fully future-proof for chipnet support.

New%20session%20-%202026-09-09T09%3A16%3A15.498Z
opencode session  |  github run

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Review: feat/cauldron-platform-fee

Overall verdict: Approve.

The PR adds a conservative, well-gated platform fee to Cauldron swaps. I independently verified the branch builds cleanly and all tests pass.

Verified locally

  • npm run build
  • npm test104/104 pass

What looks good

  • Safe degradation: The fee is only charged when the address is configured, the live BCH price is fetchable, and the computed fee is ≥ 546 sats. Any failure returns null, so older clients and endpoint outages result in no fee rather than a crash.
  • Address validation before signing: createInputAndOutput() decodes the fee address with cashAddressToLockingBytecode and throws Invalid platform fee address if it is malformed. This prevents signing a payout to an invalid address even if the watchtower endpoint were compromised.
  • No secret leakage: formatQuote() prints the fee amount, never the fee address. The address only appears in a validation error string, which is acceptable for a public CashAddress.
  • Dust-aware output threshold: PLATFORM_FEE_DUST_LIMIT = 546n matches the standard P2PKH dust threshold, preventing unrelayable outputs.
  • Double-counting bug fixed: The redundant satoshisToSupply += platformFee.amount + BigInt(getOutputSize(platformFee)) that existed in the payout block has been removed in this branch. The fee amount and output size are now added exactly once during supply sizing.
  • isChipnet propagated correctly: estimateSwap() passes isChipnet to both fetchCauldronFee() and getBchUsdPrice(), and the CLI layer already blocks chipnet swaps. This gap from the prior review is resolved.

Minor notes (non-blocking)

  1. Floating-point → bigint conversion in the USD cap (swap.ts lines 111–112)
    BigInt(Math.round(feeConfig.maxUsd * 1e8)) and BigInt(Math.round(bchUsdPrice * 1e8)) convert JS numbers to bigint. For the default cap of $1 and typical BCH prices this is fully safe, but if the server ever raises the cap to a very large value or BCH drops to a microscopic price, integer precision above Number.MAX_SAFE_INTEGER could be lost. Computing the cap entirely in bigint space would be more robust, though unlikely to matter in practice.

  2. Fee address trust model
    The fee address is accepted as-is from the watchtower endpoint without additional verification (e.g., against a hardcoded expected address). If the endpoint were compromised, an attacker could redirect fees up to the 1 USD cap per swap. This is an inherent design choice for a server-configurable address, and the cap limits exposure.

  3. Address validation timing
    An invalid fee address is caught in createInputAndOutput() at transaction-building time rather than during quote estimation. This means estimateSwap() can return a quote that later fails to build. Validating the address earlier (e.g., in fetchCauldronFee or computePlatformFee) would give slightly better UX, but it is not a functional or security issue.

Bottom line

The platform fee logic is safe, well-tested, and correctly degrades when dependencies are unavailable. Ready to merge.

New%20session%20-%202026-09-09T09%3A33%3A39.386Z
opencode session  |  github run

@joemarct
joemarct merged commit 1adcb79 into master Sep 9, 2026
1 check passed
@joemarct
joemarct deleted the feat/cauldron-platform-fee branch September 9, 2026 11:38
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