Skip to content

✨ server: add bridge offramp#1024

Draft
mainqueg wants to merge 1 commit into
mainfrom
offramp
Draft

✨ server: add bridge offramp#1024
mainqueg wants to merge 1 commit into
mainfrom
offramp

Conversation

@mainqueg
Copy link
Copy Markdown
Member

@mainqueg mainqueg commented May 19, 2026

Summary by CodeRabbit

  • New Features

    • Bridge offramp: crypto withdrawals to external accounts
    • External account management: create, view, update, delete
    • Added Optimism network support
    • Offramp receipts trigger new "Offramp" analytics events
  • Localization

    • Spanish and Portuguese: new messages for "Withdrawal in progress" and withdrawn-amount rendering
  • Tests

    • Expanded test coverage for offramp flows and external-account operations

Review Change Stack

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

🦋 Changeset detected

Latest commit: 846895d

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 May 19, 2026

Warning

Rate limit exceeded

@mainqueg has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 27 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5fda84a2-9328-4666-89f7-1c5b2186c704

📥 Commits

Reviewing files that changed from the base of the PR and between eb56160 and 846895d.

📒 Files selected for processing (10)
  • .changeset/swift-bridges-cross.md
  • server/api/ramp.ts
  • server/hooks/bridge.ts
  • server/i18n/es.json
  • server/i18n/pt.json
  • server/test/api/ramp.test.ts
  • server/test/hooks/bridge.test.ts
  • server/test/utils/bridge.test.ts
  • server/utils/ramps/bridge.ts
  • server/utils/segment.ts

Walkthrough

Adds Bridge provider offramp support: external-account CRUD, offramp-aware /quote responses (including Optimism deposit info), transfer/static-template helpers, webhook transfer handling with Offramp analytics and push notifications, strengthened response validation, tests, i18n, and a release changeset.

Changes

Bridge Offramp Implementation

Layer / File(s) Summary
Offramp Contracts & Schemas
server/api/ramp.ts, server/utils/ramps/bridge.ts
Adds error codes for external accounts, DepositDetails.OPTIMISM, ProviderInfo.offramp, external-account/transfer schemas, and request safeParse validation with ValiError and Sentry validation context.
External Account CRUD Operations
server/utils/ramps/bridge.ts
Implements createExternalAccount, updateExternalAccount, getExternalAccount, listExternalAccounts with endorsement gating, mapping to Bridge payloads, and exports CurrencyByEndorsement.
Provider getProvider Updates
server/utils/ramps/bridge.ts
getProvider now returns offramp.currencies across status branches; ACTIVE computes approvedCurrencies from approved endorsements with Sentry warnings.
Transfer / Static Template Helpers
server/utils/ramps/bridge.ts
Adds createTransfer, getTransfers, getStaticTemplates with pagination and idempotency support.
Offramp Deposit & Transfer Flow
server/utils/ramps/bridge.ts
getOfframpDepositDetails resolves external account and templates, creates transfers when needed, validates destination addresses, and returns Optimism deposit info; removeExternalAccount deletes awaiting_funds transfers before account deletion.
Quote Endpoint & External Account Routes
server/api/ramp.ts
Refactors /quote validator into explicit Mantéca/bridge variants (onramp/offramp/crypto), rewrites bridge quote branching (direction/network), and adds authenticated POST/GET/PATCH/DELETE /external-account routes with credential/customer gating and bridge error-code translation.
Webhook Handler for Transfer Events
server/hooks/bridge.ts
Extends webhook validation to accept external_account.* and transfer.* events and adds handling for transfer.updated.status_transitioned to send offramp push notifications and track Offramp analytics on completion.
API Endpoint Tests
server/test/api/ramp.test.ts
Updates provider info tests to include offramp.currencies; crypto quote tests now expect default quote (1.0); adds bridge offramp quote tests and delete external-account tests covering errors and success.
Webhook Handler Tests
server/test/hooks/bridge.test.ts
Adds no-op event tests; tests for funds_received (notification) and payment_processed (notification + Offramp analytics), push-failure Sentry capture, credential-not-found edge case, and fixtures.
Bridge Utility Tests
server/test/utils/bridge.test.ts
Updates getProvider expectations to assert offramp.currencies; adds comprehensive external-account/offramp tests (getExternalAccount, getOfframpDepositDetails, removeExternalAccount, create/update/list).
Internationalization
server/i18n/es.json, server/i18n/pt.json
Adds Spanish and Portuguese translations for withdrawal states and withdrawn-amount rendering ("Withdrawal in progress", "Your funds are on the way to your bank", "{{amount}} {{asset}} withdrawn").
Release Notes
.changeset/swift-bridges-cross.md
Adds patch changeset documenting "✨ add bridge offramp".
Segment Offramp Analytics
server/utils/segment.ts
Adds Offramp event type to track union with properties { amount, currency, provider, source, usdcAmount }.

Sequence Diagram

sequenceDiagram
  participant Client
  participant API as /quote Endpoint
  participant BridgeUtil as bridge.getOfframpDepositDetails
  participant BridgeAPI as Bridge API
  participant Webhook as Webhook Handler
  participant Push as Push Notifications
  participant Segment as Segment Analytics

  Client->>API: POST /quote (offramp, externalAccountId)
  API->>BridgeUtil: getOfframpDepositDetails(externalAccountId, account, customer)
  BridgeUtil->>BridgeAPI: getExternalAccount(customerId, externalAccountId)
  BridgeAPI-->>BridgeUtil: external account data
  BridgeUtil->>BridgeAPI: getStaticTemplates(customerId)
  BridgeAPI-->>BridgeUtil: static templates
  BridgeUtil->>BridgeAPI: createTransfer (if no matching template)
  BridgeAPI-->>BridgeUtil: transfer created
  BridgeUtil-->>API: offramp deposit details (Optimism address)
  API-->>Client: quote + depositInfo

  BridgeAPI->>Webhook: transfer.updated.status_transitioned (funds_received)
  Webhook->>Push: Send "Withdrawal in progress" notification
  Push-->>Client: Notification delivered

  BridgeAPI->>Webhook: transfer.updated.status_transitioned (payment_processed)
  Webhook->>Segment: track(Offramp with amount, currency, usdcAmount)
  Webhook->>Push: Send "Your funds are on the way to your bank" notification
  Push-->>Client: Completion notification
  Webhook-->>BridgeAPI: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • exactly/exa#861: Modifies server/hooks/bridge.ts webhook routing logic that this PR extends with transfer/external_account events.
  • exactly/exa#680: Introduced earlier /ramp endpoints and bridge onramp logic that this PR extends for offramp/external-account behavior.
  • exactly/exa#940: Related changes to server/utils/ramps/bridge.ts provider handling and status branching that overlap with this PR's getProvider updates.

Suggested reviewers

  • nfmelendez
  • cruzdanilo
  • dieguezguille
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding bridge offramp functionality to the server, which is evident from the substantial additions across API routes, external account management, webhook handling, and deposit details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch offramp

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.

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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 188fbc9c-c51a-4982-af69-203c14ad6608

📥 Commits

Reviewing files that changed from the base of the PR and between 4a80e4e and a27f7ee.

📒 Files selected for processing (10)
  • .changeset/swift-bridges-cross.md
  • server/api/ramp.ts
  • server/hooks/bridge.ts
  • server/i18n/es.json
  • server/i18n/pt.json
  • server/test/api/ramp.test.ts
  • server/test/hooks/bridge.test.ts
  • server/test/utils/bridge.test.ts
  • server/utils/ramps/bridge.ts
  • server/utils/segment.ts

Comment thread .changeset/swift-bridges-cross.md
Comment thread server/utils/ramps/bridge.ts Outdated
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces bridge offramp functionality, allowing users to withdraw funds to external bank accounts. Key changes include new API endpoints for managing external accounts (POST, GET, PATCH, DELETE /external-account), support for offramp directions in the ramp API, and handling of Bridge webhook events for transfer status updates. The update also includes push notifications for withdrawal progress and Segment tracking for offramp events. Feedback focuses on relaxing validation constraints for street addresses and bank account numbers to accommodate shorter valid inputs, as well as refactoring repeated authentication and customer retrieval logic into a shared middleware.

Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts Outdated
Comment thread server/api/ramp.ts
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.

Actionable comments posted: 9


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86f72493-a0b9-4f8a-89db-f41aa19e65b9

📥 Commits

Reviewing files that changed from the base of the PR and between a27f7ee and eb56160.

📒 Files selected for processing (10)
  • .changeset/swift-bridges-cross.md
  • server/api/ramp.ts
  • server/hooks/bridge.ts
  • server/i18n/es.json
  • server/i18n/pt.json
  • server/test/api/ramp.test.ts
  • server/test/hooks/bridge.test.ts
  • server/test/utils/bridge.test.ts
  • server/utils/ramps/bridge.ts
  • server/utils/segment.ts

Comment thread server/api/ramp.ts
Comment thread server/test/hooks/bridge.test.ts
Comment thread server/test/utils/bridge.test.ts
Comment thread server/test/utils/bridge.test.ts Outdated
Comment thread server/test/utils/bridge.test.ts Outdated
Comment on lines +351 to +385
export function updateExternalAccount(
customer: InferOutput<typeof CustomerResponse>,
externalAccountId: string,
update: InferInput<typeof UpdateExternalAccountInput>,
) {
return request(
BridgeExternalAccount,
`/customers/${customer.id}/external_accounts/${externalAccountId}`,
{},
{
address: update.address && {
street_line_1: update.address.streetLine1,
street_line_2: update.address.streetLine2,
city: update.address.city,
state: update.address.state,
postal_code: update.address.postalCode,
country: update.address.country,
},
account: update.account && {
checking_or_savings: update.account.checkingOrSavings,
routing_number: update.account.routingNumber,
},
} satisfies InferInput<typeof UpdateExternalAccount>,
"PUT",
).then(
(externalAccount) =>
({
addressValid: externalAccount.beneficiary_address_valid,
bankName: externalAccount.bank_name,
currency: parse(picklist(FiatCurrency), OfframpFiat[externalAccount.currency]),
id: externalAccount.id,
ownerName: externalAccount.account_owner_name,
}) satisfies InferOutput<typeof ExternalAccount>,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" | head -20

Repository: exactly/exa

Length of output: 609


🏁 Script executed:

cat server/utils/ramps/bridge.ts | head -50

Repository: exactly/exa

Length of output: 1269


🏁 Script executed:

wc -l server/utils/ramps/bridge.ts

Repository: exactly/exa

Length of output: 89


🏁 Script executed:

rg "getExternalAccount|removeExternalAccount" server/utils/ramps/bridge.ts -A 20

Repository: exactly/exa

Length of output: 2715


🏁 Script executed:

rg "NOT_FOUND|EXTERNAL_ACCOUNT_NOT_FOUND" server/utils/ramps/ -B 2 -A 2

Repository: exactly/exa

Length of output: 3748


🏁 Script executed:

rg "ServiceError" server/utils/ramps/bridge.ts -B 3 -A 5

Repository: exactly/exa

Length of output: 2769


🏁 Script executed:

rg "ErrorCodes\." server/utils/ramps/bridge.ts

Repository: exactly/exa

Length of output: 2643


🏁 Script executed:

sed -n '351,385p' server/utils/ramps/bridge.ts

Repository: exactly/exa

Length of output: 1294


Add 404 normalization to updateExternalAccount() for consistency.

getExternalAccount() and removeExternalAccount() normalize Bridge 404s to ErrorCodes.EXTERNAL_ACCOUNT_NOT_FOUND, but updateExternalAccount() lets the ServiceError escape. This causes the API endpoint to return 500 for stale/deleted external account IDs instead of the proper error response.

Suggested normalization
 export function updateExternalAccount(
   customer: InferOutput<typeof CustomerResponse>,
   externalAccountId: string,
   update: InferInput<typeof UpdateExternalAccountInput>,
 ) {
   return request(
     BridgeExternalAccount,
     `/customers/${customer.id}/external_accounts/${externalAccountId}`,
     {},
     {
       address: update.address && {
         street_line_1: update.address.streetLine1,
         street_line_2: update.address.streetLine2,
         city: update.address.city,
         state: update.address.state,
         postal_code: update.address.postalCode,
         country: update.address.country,
       },
       account: update.account && {
         checking_or_savings: update.account.checkingOrSavings,
         routing_number: update.account.routingNumber,
       },
     } satisfies InferInput<typeof UpdateExternalAccount>,
     "PUT",
-  ).then(
+  )
+    .catch((error: unknown) => {
+      if (
+        error instanceof ServiceError &&
+        typeof error.cause === "string" &&
+        error.cause.includes(BridgeApiErrorCodes.NOT_FOUND)
+      ) {
+        throw new Error(ErrorCodes.EXTERNAL_ACCOUNT_NOT_FOUND);
+      }
+      throw error;
+    })
+    .then(
     (externalAccount) =>
       ({
         addressValid: externalAccount.beneficiary_address_valid,
         bankName: externalAccount.bank_name,
         currency: parse(picklist(FiatCurrency), OfframpFiat[externalAccount.currency]),
         id: externalAccount.id,
         ownerName: externalAccount.account_owner_name,
       }) satisfies InferOutput<typeof ExternalAccount>,
   );
 }

Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts
@sentry
Copy link
Copy Markdown

sentry Bot commented May 19, 2026

✅ All tests 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