Skip to content

✨ server: add bridge fee window tracking#925

Open
mainqueg wants to merge 1 commit intowindow-rulefrom
bridge-fees
Open

✨ server: add bridge fee window tracking#925
mainqueg wants to merge 1 commit intowindow-rulefrom
bridge-fees

Conversation

@mainqueg
Copy link
Copy Markdown
Member

@mainqueg mainqueg commented Mar 30, 2026

Summary by CodeRabbit

  • New Features

    • Bridge fee window tracking and sponsored-fee reporting added; deposit responses now include sponsored-fees (available volume/count, symbol, window).
    • Controls to enable/disable and update bridge-sponsored fees with automated windowed rules; tracking emits events when windows trigger.
  • Bug Fixes

    • More accurate per-rail deposit fee values; stricter webhook date and amount validation to reject invalid timestamps/amounts.
  • Tests

    • Expanded coverage and lifecycle cleanup for fee-window behavior, reporting paths, and webhook validation.

Open with Devin

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 30, 2026

🦋 Changeset detected

Latest commit: 4f99113

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduces windowed sponsored-fee tracking for bridge flows: adds feeRule exports and enabling/disabling logic, surfaces sponsoredFees in deposit responses, validates webhook timestamps and numeric amounts, reports events to the fee rule, and wires lifecycle/teardown for feeRule and Redis in server and tests.

Changes

Cohort / File(s) Summary
Release Configuration
/.changeset/swift-foxes-track.md
New changeset declaring a patch release for @exactly/server with note "add bridge fee window tracking".
Provider API Schema
server/api/ramp.ts
Added SponsoredFees Valibot schema and included optional sponsoredFees on multiple DepositDetails variants; updated Valibot imports to include number.
Webhook Event Reporting
server/hooks/bridge.ts
Tightened validators to require event_object.created_at parse as Date; parse/validate USDC amounts (reject NaN), call/await feeRule.report(...) in relevant branches; minor import additions.
Server Lifecycle Management
server/index.ts
Added feeRule.stop() to shutdown sequence; made shutdown .then async and await closeRedis() after Promise.allSettled.
Fee Window Core Logic
server/utils/ramps/bridge.ts
Added windowed fee rule (feeRule) and exports (fees, enableFees, disableFees, volumeThreshold, countThreshold, feeWindow), renamed fee fields (developer_fee_percentagedeveloper_fee_percent), added custom_developer_fee_percent, added setFee/evaluateSponsoredFees, integrated captureEvent, and include computed sponsoredFees in deposit builders.
Tests: Integration & Unit
server/test/api/ramp.test.ts, server/test/hooks/bridge.test.ts, server/test/utils/bridge.test.ts
Added async Redis cleanup and feeRule lifecycle hooks, introduced defaultSponsoredFees() helper, updated fixtures/assertions to include sponsoredFees, added tests for invalid created_at/amounts, feeRule.report success/failure, sponsored-fees evaluation, and enable/disable fee API behavior.

Sequence Diagram

sequenceDiagram
    participant Webhook as Bridge Webhook
    participant Hook as Webhook Handler
    participant FeeRule as Fee Rule (feeRule)
    participant Redis as Redis (storage)
    participant API as Provider API / Bridge helpers

    Webhook->>Hook: POST event (payment_processed / payment_submitted / drain)
    Hook->>Hook: validate event_object.created_at (must parse to Date)
    Hook->>Hook: parse numeric amount (receipt.final_amount / outgoing_amount)
    alt amount is NaN
        Hook->>Hook: captureException & return { code: "invalid amount" }
    else amount valid
        Hook->>FeeRule: report({ bridgeId, eventId, amount: Math.round(amount*100), timestamp })
        FeeRule->>Redis: increment/record volume & count for window
        FeeRule->>FeeRule: evaluate thresholds
        alt Threshold exceeded
            FeeRule->>API: enableFees(customerId) (PUT VAs & liquidation addresses)
            FeeRule->>API: captureEvent("bridge fees enabled")
        else Threshold not exceeded
            FeeRule->>FeeRule: continue accumulating
        end
        Hook->>Hook: proceed with notifications/tracking after report
    end
    API->>FeeRule: evaluateSponsoredFees(customerId) when building deposit details
    FeeRule->>Redis: read current window state
    FeeRule->>API: return sponsoredFees (window, available, thresholds) or undefined on error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • cruzdanilo
  • nfmelendez
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'add bridge fee window tracking' directly matches the core functionality added across the changeset, which implements fee window tracking and fee rule management for bridge transactions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bridge-fees

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 89.83051% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.09%. Comparing base (0f204ad) to head (4f99113).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/utils/ramps/bridge.ts 85.36% 5 Missing and 1 partial ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           window-rule     #925      +/-   ##
===============================================
+ Coverage        72.01%   72.09%   +0.08%     
===============================================
  Files              229      229              
  Lines             8400     8436      +36     
  Branches          2695     2701       +6     
===============================================
+ Hits              6049     6082      +33     
- Misses            2114     2119       +5     
+ Partials           237      235       -2     
Flag Coverage Δ
e2e 71.79% <74.57%> (-0.19%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

coderabbitai[bot]

This comment was marked as resolved.

@mainqueg mainqueg force-pushed the window-rule branch 3 times, most recently from 54d0250 to 295d566 Compare April 7, 2026 15:24
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
server/utils/ramps/bridge.ts (2)

422-429: ⚠️ Potential issue | 🟠 Major

Consult the fee window before creating a new rail.

These branches create the instruction first and only then read evaluateSponsoredFees(...). If the customer already exhausted sponsorship on another rail, the new account/address is still created fee-free, so the response can come back with fee: "0.0" while sponsoredFees.available is already 0.

Also applies to: 456-468


1148-1158: ⚠️ Potential issue | 🔴 Critical

Normalize volume before comparing it to a USD threshold.

This rule stores only a raw amount string, but it is fed from native-currency Bridge events and later surfaced as USD. Math.round(Number(event.amount)) * 100n also drops cent precision. That means fee activation can move materially on non-USD or fractional transfers.

Run this to confirm the rule has no currency context and currently rounds to whole dollars:

#!/bin/bash
rg -n -C2 'schema: object\(|symbol: "USD"|Math\.round\(Number\(event\.amount\)\) \* 100n' server/utils/ramps/bridge.ts
rg -n -C3 'feeRule\.report\(|initial_amount|outgoing_amount|currency:' server/hooks/bridge.ts
python - <<'PY'
import math
for amount in ["0.49", "0.50", "99.999", "200.01"]:
    cents = math.floor(float(amount) + 0.5) * 100
    print(f"{amount} -> current cents: {cents}")
PY

Expected: the grep output shows the window is labeled USD while the stored event has only amount, and the Python output shows current whole-dollar rounding.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 578da5c6-fb6a-41e5-9e56-908634d46066

📥 Commits

Reviewing files that changed from the base of the PR and between 2c738cc and 4a70ba9.

📒 Files selected for processing (8)
  • .changeset/swift-foxes-track.md
  • server/api/ramp.ts
  • server/hooks/bridge.ts
  • server/index.ts
  • server/test/api/ramp.test.ts
  • server/test/hooks/bridge.test.ts
  • server/test/utils/bridge.test.ts
  • server/utils/ramps/bridge.ts

@mainqueg mainqueg marked this pull request as ready for review April 7, 2026 21:34
sentry[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Comment on lines +326 to 329
Promise.allSettled([feeRule.stop(), closeSentry(), closeSegment(), database.$client.end()])
.then(async (results) => {
await closeRedis();
if (error) reject(error);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The server shutdown sequence can hang indefinitely if a BullMQ job is stuck, preventing Redis connections from being closed.
Severity: HIGH

Suggested Fix

Wrap the feeRule.stop() call in a Promise.race with a timeout. Alternatively, use BullMQ's force-close options to ensure the worker is terminated after a reasonable period, allowing the shutdown sequence to proceed and close all connections.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: server/index.ts#L326-L329

Potential issue: The `feeRule.stop()` method calls `worker.close()` without a timeout.
Since `worker.close()` waits indefinitely for active jobs to finish, a single hanging
job (e.g., waiting on an external API) will block the promise from resolving. This
prevents the `Promise.allSettled` in the main shutdown sequence from completing, meaning
`closeRedis()` is never called. This results in a resource leak and prevents a graceful
server shutdown.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
server/utils/ramps/bridge.ts (1)

422-429: ⚠️ Potential issue | 🟠 Major

Seed newly created Bridge rails from the current fee-window state.

Both provisioning paths still create new rails with zero fees. If the customer already crossed the sponsored-fee threshold before their first ACH/WIRE/SEPA/SPEI/PIX-BR or crypto rail is created, /quote can return exhausted sponsoredFees while the freshly created rail still has fee: "0.0", and that rail stays free until a later trigger/expiry cycle updates it.

🛠️ fix sketch
+  const fee = await feeRule
+    .read(customer.id)
+    .then(({ result }) => (result.trigger ? fees[CurrencyToBridge[currency]] : "0.0"))
+    .catch(() => "0.0");
   virtualAccount ??= await createVirtualAccount(customer.id, {
     source: { currency: CurrencyToBridge[currency] },
-    developer_fee_percent: "0.0",
+    developer_fee_percent: fee,
     destination: { currency: "usdc", payment_rail: supportedChainId, address: account },
   });

Apply the same fee derivation to custom_developer_fee_percent when creating liquidation addresses, or have evaluateSponsoredFees() return the trigger state so both branches can reuse a single read.

Also applies to: 456-468


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 52045bdb-0a91-4aa5-9c4b-4050b157b5fa

📥 Commits

Reviewing files that changed from the base of the PR and between 31cdf8d and 4f99113.

📒 Files selected for processing (8)
  • .changeset/swift-foxes-track.md
  • server/api/ramp.ts
  • server/hooks/bridge.ts
  • server/index.ts
  • server/test/api/ramp.test.ts
  • server/test/hooks/bridge.test.ts
  • server/test/utils/bridge.test.ts
  • server/utils/ramps/bridge.ts

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