Skip to content

✨ server: add bridge offramp#1024

Open
mainqueg wants to merge 1 commit into
mainfrom
offramp
Open

✨ 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, Optimism deposit support, and default quote fallbacks for certain crypto/chain combos
    • External account management API: create, list, update, delete
  • Analytics & Notifications

    • New "Offramp" analytics event and push notifications for withdrawal states ("Withdrawal in progress", "Withdraw completed")
  • Localization

    • Spanish and Portuguese: added withdrawal messages and withdrawn-amount rendering
  • Tests

    • Expanded coverage for offramp flows, external-account ops, provider/quote behaviors, and webhook events

Review Change Stack

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

🦋 Changeset detected

Latest commit: ba90b46

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

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

<review_stack_artifact>

</review_stack_artifact>

🚥 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 '✨ server: add bridge offramp' directly describes the main objective of the pull request—adding bridge offramp functionality—which is supported by extensive changes across multiple files including new external-account routes, offramp deposit helpers, and related webhooks.
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
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code 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 thread server/utils/ramps/bridge.ts
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

Codecov Report

❌ Patch coverage is 94.17219% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.97%. Comparing base (63373eb) to head (ba90b46).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/api/ramp.ts 87.70% 17 Missing and 6 partials ⚠️
server/utils/ramps/bridge.ts 96.27% 12 Missing and 8 partials ⚠️
server/hooks/bridge.ts 96.77% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1024      +/-   ##
==========================================
+ Coverage   73.87%   74.97%   +1.10%     
==========================================
  Files         243      243              
  Lines       10852    11542     +690     
  Branches     3678     3972     +294     
==========================================
+ Hits         8017     8654     +637     
- Misses       2528     2564      +36     
- Partials      307      324      +17     
Flag Coverage Δ
e2e 73.76% <89.66%> (-0.12%) ⬇️

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.

@mainqueg mainqueg force-pushed the offramp branch 3 times, most recently from 846895d to f4612a6 Compare May 20, 2026 17:15
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/hooks/bridge.ts (1)

132-193: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Short-circuit ignored transfer events before credential lookup.

transfer.updated.status_transitioned events that you later ignore (external_account_id == null or any state other than funds_received / payment_processed) still resolve bridgeId and can return { code: "credential not found" } first. That turns an intended no-op into Sentry noise whenever on_behalf_of is unpaired.

🛠️ Suggested guard placement
+    if (
+      payload.event_type === "transfer.updated.status_transitioned" &&
+      (!payload.event_object.destination.external_account_id ||
+        (payload.event_object.state !== "funds_received" &&
+          payload.event_object.state !== "payment_processed"))
+    ) {
+      return c.json({ code: "ok" }, 200);
+    }
+
     const bridgeId =
       payload.event_type === "customer.updated.status_transitioned"
         ? payload.event_object.id
         : payload.event_type === "transfer.updated.status_transitioned"
           ? payload.event_object.on_behalf_of

Also applies to: 259-293


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd0dcddd-cfdb-437b-8f7c-5d34cf75216d

📥 Commits

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

📒 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
@mainqueg mainqueg marked this pull request as ready for review May 20, 2026 17:34
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4612a6090

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/utils/ramps/bridge.ts Outdated
Comment thread server/api/ramp.ts
Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e083f4f853

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/hooks/bridge.ts
Comment thread server/utils/ramps/bridge.ts Outdated
Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.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: 6

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

780-798: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pass a stable idempotency key when creating the static template.

This read-then-create branch is still race-prone. Two concurrent requests can both miss templates.find(...) and create separate awaiting_funds transfers because createTransfer() is called without a deterministic key. removeExternalAccount() only deletes one of them.

Proposed fix
   const templates = await getStaticTemplates(customer.id);
+  const idempotencyKey = crypto
+    .createHash("sha256")
+    .update(["offramp", customer.id, externalAccountId, supportedChain, account.toLowerCase()].join(":"))
+    .digest("hex");
   let transfer = templates.find(
     ({ destination, source }) =>
       destination.external_account_id === externalAccountId &&
       source.payment_rail === supportedChain &&
       source.currency === "usdc",
   );

   transfer ??= await createTransfer({
     on_behalf_of: customer.id,
     client_reference_id: account,
     source: { currency: "usdc", payment_rail: supportedChain },
     destination: {
       currency: externalAccount.currency,
       payment_rail: paymentRail,
       external_account_id: externalAccountId,
     },
     features: { flexible_amount: true, static_template: true, allow_any_from_address: true },
-  });
+  }, idempotencyKey);
Does Bridge require an `Idempotency-Key` header for POST transfer creation, and for how long does it deduplicate repeated requests?

1667-1670: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize the validation context before sending it to Sentry.

Spreading result into setContext() can send raw Bridge payloads on schema failures. In this module that includes bank details, addresses, and transfer metadata, so this is a privacy/compliance leak. Record only sanitized issues plus request metadata.

Proposed fix
   const result = safeParse(schema, JSON.parse(new TextDecoder().decode(rawBody)));
   if (!result.success) {
-    setContext("validation", { ...result, flatten: flatten(result.issues) });
+    setContext("validation", { url, issues: flatten(result.issues) });
     throw new ValiError(result.issues);
   }
Does valibot `safeParse` include the original `input` in the failure result object it returns?

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75cb7714-1d84-4f55-a9f8-e920d6faf07a

📥 Commits

Reviewing files that changed from the base of the PR and between f4612a6 and e083f4f.

📒 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/hooks/bridge.ts
Comment thread server/test/api/ramp.test.ts
Comment thread server/test/api/ramp.test.ts
Comment thread server/test/hooks/bridge.test.ts
Comment thread server/utils/ramps/bridge.ts
@mainqueg mainqueg force-pushed the offramp branch 2 times, most recently from 45c2696 to db233d9 Compare May 21, 2026 13:44
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/utils/ramps/bridge.ts (1)

1643-1675: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Remove commented-out debug code.

Line 1673 contains commented-out debug code that should be removed per coding guidelines: "do not use comments. the only exceptions are static analysis annotations...and TODO/HACK/FIXME markers."

Suggested fix
   if (!result.success) {
     setContext("validation", { ...result, flatten: flatten(result.issues) });
     throw new ValiError(result.issues);
   }
-  // console.dir(result.output, { depth: null });
   return result.output;

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e74768f0-928f-4a94-977f-ba0cf6902b21

📥 Commits

Reviewing files that changed from the base of the PR and between e083f4f and db233d9.

📒 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/test/utils/bridge.test.ts
Comment thread server/utils/ramps/bridge.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db233d97c7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/utils/ramps/bridge.ts
Comment thread server/hooks/bridge.ts
Comment thread server/utils/ramps/bridge.ts Outdated
Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f405766bb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/utils/ramps/bridge.ts Outdated
Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts
Comment thread server/hooks/bridge.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46470e2b12

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts Outdated
Comment thread server/utils/ramps/bridge.ts
@cruzdanilo cruzdanilo linked an issue May 21, 2026 that may be closed by this pull request
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cd8eeef86

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/utils/ramps/bridge.ts
Comment thread server/api/ramp.ts Outdated
Comment thread server/hooks/bridge.ts
Comment thread server/utils/ramps/bridge.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2980c839ae

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/utils/ramps/bridge.ts
Comment thread server/utils/ramps/bridge.ts Outdated
Comment on lines +342 to +345
bankName: externalAccount.bankName,
currency: externalAccount.currency,
id,
ownerName: externalAccount.accountOwnerName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return canonical external-account fields from Bridge response

Map bankName and ownerName from the BridgeExternalAccount response instead of echoing request input. The current code returns externalAccount.bankName / externalAccount.accountOwnerName, so if the client omits bankName or Bridge normalizes/corrects values, the API responds with stale or missing data even though Bridge returned the authoritative fields. This can immediately desync UI state after account creation.

Useful? React with 👍 / 👎.

Comment thread server/hooks/bridge.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 617580bc0d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread server/utils/ramps/bridge.ts Outdated
],
},
onramp: { currencies: [...currencies, ...approvedCurrencies] },
offramp: { currencies: [{ currency: "USDT" as const, network: "TRON" as const }, ...approvedCurrencies] },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Advertise USDC offramp rails in bridge provider output

getCryptoOfframpDepositDetails and the /quote validator support USDC offramps on BASE, SOLANA, and STELLAR, but this provider payload only exposes { currency: "USDT", network: "TRON" } plus fiat currencies. Clients that rely on /ramp capabilities to render available flows will not surface the supported USDC crypto offramp routes, making part of the new offramp functionality effectively unreachable.

Useful? React with 👍 / 👎.

Comment thread server/utils/ramps/bridge.ts
Comment on lines +782 to +789
let transfer = templates.find(
({ destination, source, state }) =>
destination.external_account_id === externalAccountId &&
source.payment_rail === supportedChain &&
source.currency === "usdc" &&
state !== "canceled",
);
if (transfer && transfer.state !== "awaiting_funds") throw new Error(ErrorCodes.TRANSFER_IN_USE);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check all matching templates before reusing offramp route

getOfframpDepositDetails inspects only the first non-canceled matching template, then decides whether to throw TRANSFER_IN_USE. If multiple matching templates exist and the first is awaiting_funds while another is already in-flight (for example funds_received), this path will incorrectly allow reuse and return deposit instructions instead of blocking the new withdrawal attempt.

Useful? React with 👍 / 👎.

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.

server: off ramp fiat and crypto with bridge

1 participant