Skip to content

✨ server: automate firewall allow after kyc approval - #1225

Open
cruzdanilo wants to merge 3 commits into
mainfrom
allow
Open

✨ server: automate firewall allow after kyc approval#1225
cruzdanilo wants to merge 3 commits into
mainfrom
allow

Conversation

@cruzdanilo

@cruzdanilo cruzdanilo commented Aug 14, 2026

Copy link
Copy Markdown
Member

closes #643

Summary by CodeRabbit

  • New Features
    • Added background processing for card credit activation after eligible account activity.
    • Added automated firewall access after successful KYC approval.
    • Improved asset activity processing with balance updates, funding actions, notifications, and follow-up credit processing.
  • Bug Fixes
    • Card creation now succeeds even when background credit processing cannot be queued.
    • Added retry and failure handling for background processing.
  • Localization
    • Updated Spanish and Portuguese messages for asset updates, credit mode, and available funds.

@cruzdanilo
cruzdanilo requested a review from nfmelendez as a code owner August 14, 2026 15:54
@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c34c470

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

This PR includes changesets to release 2 packages
Name Type
@exactly/infra Minor
@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

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

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

The server adds allow, poke, and credit queues with supervised workers. Persona and activity hooks enqueue work. Card creation queues credit processing. Runtime wiring, tests, translations, and release metadata are updated.

Changes

Queue worker orchestration

Layer / File(s) Summary
Worker contracts and runtime wiring
server/workers/*/job.ts, server/workers/*/queue.ts, server/workers/*/bin.ts, infra/utils/modules.ts, server/index.ts, server/api/index.ts, server/script/openapi.ts, server/test/e2e.ts, server/test/workers/bin.test.ts
Adds allow, credit, and poke job contracts, BullMQ queues, worker bootstraps, runtime wiring, shutdown handling, infrastructure modules, and startup tests.
Firewall authorization and follow-up poke
server/hooks/persona.ts, server/workers/allow/worker.ts, server/test/hooks/persona.test.ts, server/test/workers/allow.test.ts, server/test/mocks/deployments.ts
Persona approval enqueues validated firewall authorization data. The allow worker authorizes the account and queues a follow-up poke job.
Activity-driven poke processing
server/hooks/activity.ts, server/workers/poke/*, server/test/hooks/activity.test.ts, server/test/workers/poke.test.ts
Activity processing queues deduplicated account assets. The poke worker handles account setup, balances, pokes, retries, notifications, and credit enqueueing.
Credit processing and card integration
server/api/card.ts, server/workers/credit/*, server/utils/panda.ts, server/i18n/*.json, server/test/api/card.test.ts, server/test/workers/credit.test.ts
Card creation queues credit processing instead of calling autoCredit. The credit worker evaluates deposits, changes eligible card modes, and sends localized notifications.
Release metadata
.changeset/*.md
Adds release notes for the allow, poke, and credit worker changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟠 High · up to 93096

The PR adds asynchronous firewall and credit-processing behavior, but the current implementation can silently skip distinct processing jobs, permanently lose credit work after persistence, duplicate user notifications during retries, and block user creation for malformed credentials. These correctness and availability risks make the PR unsafe to merge until addressed.

Suggested reviewers: nfmelendez, dieguezguille, franm91

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also adds credit queue processing, changes card creation, removes synchronous auto-credit logic, and updates related translations. These changes are not directly required by issue #643, which o… Move unrelated credit-processing, card API, auto-credit removal, and translation changes to a separate pull request, or document their direct dependency on issue #643.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies issue #643 by enqueuing an allow job after KYC approval, performing the firewall allow operation, and enqueueing a follow-up poke job.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: automating firewall allowance after KYC approval. It is concise and directly related to the pull request objectives.
Full details: Out of Scope Changes check

Explanation

The PR also adds credit queue processing, changes card creation, removes synchronous auto-credit logic, and updates related translations. These changes are not directly required by issue #643, which only requires poking an account after firewall allowance.

✨ 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 allow
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch allow

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 53a3d3bf-6041-4b62-b601-afe847ac3bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 1877f21 and 597de4d.

📒 Files selected for processing (37)
  • .changeset/afraid-mangos-follow.md
  • .changeset/blue-bottles-wave.md
  • .changeset/bumpy-regions-read.md
  • .changeset/free-lamps-pump.md
  • .changeset/proud-tools-sneeze.md
  • .changeset/tender-foxes-feel.md
  • infra/utils/modules.ts
  • server/api/card.ts
  • server/api/index.ts
  • server/hooks/activity.ts
  • server/hooks/persona.ts
  • server/i18n/es.json
  • server/i18n/pt.json
  • server/index.ts
  • server/test/api/api.test.ts
  • server/test/api/card.test.ts
  • server/test/e2e.ts
  • server/test/hooks/activity.test.ts
  • server/test/hooks/persona.test.ts
  • server/test/mocks/deployments.ts
  • server/test/workers/allow.test.ts
  • server/test/workers/bin.test.ts
  • server/test/workers/credit.test.ts
  • server/test/workers/poke.test.ts
  • server/utils/panda.ts
  • server/workers/allow/bin.ts
  • server/workers/allow/job.ts
  • server/workers/allow/queue.ts
  • server/workers/allow/worker.ts
  • server/workers/credit/bin.ts
  • server/workers/credit/job.ts
  • server/workers/credit/queue.ts
  • server/workers/credit/worker.ts
  • server/workers/poke/bin.ts
  • server/workers/poke/job.ts
  • server/workers/poke/queue.ts
  • server/workers/poke/worker.ts

Comment thread server/api/card.ts
Comment on lines +649 to +656
await database.insert(cards).values([{ id: card.id, credentialId, lastFour: card.last4, productId }]);
await credit.enqueue(account).catch((error: unknown) =>
captureException(error, {
level: "error",
tags: { queue: creditName, job: creditName },
extra: { account },
}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make credit publication durable.

If credit.enqueue rejects, this handler only captures the error and returns card creation success. The card is already persisted. A later request returns "already created" at Line 601 and does not publish another job. An eligible account can then remain in debit mode after a transient Redis outage.

Persist an outbox record in the same transaction as the card insert. Dispatch and retry that record until the credit queue accepts it. Update the queue-failure test to verify eventual dispatch.

Comment thread server/hooks/activity.ts
Comment on lines +172 to +184
await Promise.all(
[...pokes].map(([account, { assets, factory, publicKey, source }]) =>
poke.enqueue({
account,
assets: [...assets],
chainId: chain.id,
factory,
origin: "activity",
publicKey: bytesToHex(publicKey),
source,
}),
),
)
.then((results) => {
getActiveSpan()?.setStatus(
results.every((result) => result.status === "fulfilled")
? { code: SPAN_STATUS_OK }
: { code: SPAN_STATUS_ERROR, message: "activity_failed" },
);
})
.catch((error: unknown) => captureException(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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Awaiting the enqueue makes webhook retries resend notifications.

A rejected poke.enqueue now returns 500, so Alchemy retries the webhook. The poke job dedupes through its job id, but the push notification sent earlier in the handler does not. Each retry delivers another "Funds received" message for the same transfer. Pass a deterministic idempotencyKey to onesignal.sendPushNotification (for example, the transfer hash plus asset) so retries do not duplicate user notifications.

Comment thread server/hooks/persona.ts Outdated
Comment thread server/hooks/persona.ts
Comment on lines +374 to +385
async function enqueueAllow(current: NonNullable<typeof credential>) {
if (account.success && firewallAddress)
await allow.enqueue({
account: account.output,
chainId: chain.id,
factory: parse(Address, current.factory),
publicKey: bytesToHex(current.publicKey),
source: current.source,
});
}
if (credential.pandaId) {
await enqueueAllow(credential);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate factory the same way as account.

account uses safeParse and the code skips the enqueue when parsing fails. factory uses parse, which throws. A credential row with a malformed factory therefore returns 500, blocks panda.createUser at Line 447, and makes Persona retry the webhook indefinitely. Use safeParse for factory and report the bad row instead.

🛠️ Proposed change
       async function enqueueAllow(current: NonNullable<typeof credential>) {
-        if (account.success && firewallAddress)
-          await allow.enqueue({
-            account: account.output,
-            chainId: chain.id,
-            factory: parse(Address, current.factory),
-            publicKey: bytesToHex(current.publicKey),
-            source: current.source,
-          });
+        const factory = safeParse(Address, current.factory);
+        if (!account.success || !firewallAddress) return;
+        if (!factory.success) {
+          captureException(new Error("invalid factory address"), { level: "error", extra: { referenceId } });
+          return;
+        }
+        await allow.enqueue({
+          account: account.output,
+          chainId: chain.id,
+          factory: factory.output,
+          publicKey: bytesToHex(current.publicKey),
+          source: current.source,
+        });
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function enqueueAllow(current: NonNullable<typeof credential>) {
if (account.success && firewallAddress)
await allow.enqueue({
account: account.output,
chainId: chain.id,
factory: parse(Address, current.factory),
publicKey: bytesToHex(current.publicKey),
source: current.source,
});
}
if (credential.pandaId) {
await enqueueAllow(credential);
async function enqueueAllow(current: NonNullable<typeof credential>) {
const factory = safeParse(Address, current.factory);
if (!account.success || !firewallAddress) return;
if (!factory.success) {
captureException(new Error("invalid factory address"), { level: "error", extra: { referenceId } });
return;
}
await allow.enqueue({
account: account.output,
chainId: chain.id,
factory: factory.output,
publicKey: bytesToHex(current.publicKey),
source: current.source,
});
}
if (credential.pandaId) {
await enqueueAllow(credential);

Comment on lines +594 to +620
it("fails before panda creation when allow cannot be queued", async () => {
const error = new Error("redis unavailable");
const errorConsole = vi.spyOn(console, "error").mockImplementation(() => undefined);
allow.enqueue.mockRejectedValueOnce(error);

const response = await appClient.index.$post({
header: {
"persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
},
json: {
...validPayload,
data: {
...validPayload.data,
attributes: {
...validPayload.data.attributes,
payload: {
...validPayload.data.attributes.payload,
included: [...validPayload.data.attributes.payload.included],
},
},
},
},
});

expect(response.status).toBe(500);
expect(errorConsole).toHaveBeenCalledWith(error);
expect(allow.enqueue).toHaveBeenCalledOnce();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the postInquiry helper.

Lines 599-616 rebuild the same request that postInquiry builds at Line 2004, including the identical signature header. Call the helper to remove the duplicate payload.

♻️ Proposed refactor
-    const response = await appClient.index.$post({
-      header: {
-        "persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
-      },
-      json: {
-        ...validPayload,
-        data: {
-          ...validPayload.data,
-          attributes: {
-            ...validPayload.data.attributes,
-            payload: {
-              ...validPayload.data.attributes.payload,
-              included: [...validPayload.data.attributes.payload.included],
-            },
-          },
-        },
-      },
-    });
+    const response = await postInquiry();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("fails before panda creation when allow cannot be queued", async () => {
const error = new Error("redis unavailable");
const errorConsole = vi.spyOn(console, "error").mockImplementation(() => undefined);
allow.enqueue.mockRejectedValueOnce(error);
const response = await appClient.index.$post({
header: {
"persona-signature": "t=1733865120,v1=debbacfe1b0c5f8797a1d68e8428fba435aa4ca3b5d9a328c3c96ee4d04d84df",
},
json: {
...validPayload,
data: {
...validPayload.data,
attributes: {
...validPayload.data.attributes,
payload: {
...validPayload.data.attributes.payload,
included: [...validPayload.data.attributes.payload.included],
},
},
},
},
});
expect(response.status).toBe(500);
expect(errorConsole).toHaveBeenCalledWith(error);
expect(allow.enqueue).toHaveBeenCalledOnce();
it("fails before panda creation when allow cannot be queued", async () => {
const error = new Error("redis unavailable");
const errorConsole = vi.spyOn(console, "error").mockImplementation(() => undefined);
allow.enqueue.mockRejectedValueOnce(error);
const response = await postInquiry();
expect(response.status).toBe(500);
expect(errorConsole).toHaveBeenCalledWith(error);
expect(allow.enqueue).toHaveBeenCalledOnce();

Comment thread server/workers/poke/queue.ts
Comment thread server/workers/poke/worker.ts Outdated
Comment on lines +166 to +168
if (job.data.origin === "activity") {
await credit.enqueue(job.data.account, `poke-${job.id}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Avoid a shared credit job id when job.id is undefined.

BullMQ types job.id as string | undefined. If it is undefined, the credit job id becomes the literal "poke-undefined", which every account shares. The credit queue then treats unrelated accounts as duplicates and drops their credit processing. Fall back to the account.

🛠️ Proposed change
-          await credit.enqueue(job.data.account, `poke-${job.id}`);
+          await credit.enqueue(job.data.account, `poke-${job.id ?? job.data.account}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (job.data.origin === "activity") {
await credit.enqueue(job.data.account, `poke-${job.id}`);
}
if (job.data.origin === "activity") {
await credit.enqueue(job.data.account, `poke-${job.id ?? job.data.account}`);
}

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.63057% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.24%. Comparing base (b02e1ab) to head (c34c470).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/workers/poke/bin.ts 63.63% 4 Missing ⚠️
server/workers/allow/bin.ts 62.50% 3 Missing ⚠️
server/workers/credit/bin.ts 66.66% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1225      +/-   ##
==========================================
- Coverage   73.48%   73.24%   -0.24%     
==========================================
  Files         276      288      +12     
  Lines       13454    13397      -57     
  Branches     4732     4610     -122     
==========================================
- Hits         9887     9813      -74     
- Misses       3231     3243      +12     
- Partials      336      341       +5     
Flag Coverage Δ
e2e 73.24% <93.63%> (-0.24%) ⬇️

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

☔ View full report in Codecov by Harness.
📢 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.

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

Copy link
Copy Markdown

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: 597de4d62d

ℹ️ 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".

publicKey,
source,
}: Omit<Job, "sentryBaggage" | "sentryTrace">) {
await instance.enqueue({ account, assets, chainId, factory, publicKey, source }, account);

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 Requeue allow jobs after terminal failures

If the allow worker exhausts its retries (for example during an RPC/KMS outage), the failed job is kept by the shared queue defaults, and this stable account job id means any later Persona retry for the same approved account is treated by BullMQ as a duplicate instead of adding fresh work. The webhook will then continue as though allow was queued, but the firewall allow transaction will never run until the failed job is manually removed or expires, leaving KYC-approved accounts blocked from the firewall flow.

Useful? React with 👍 / 👎.

Comment thread server/workers/poke/queue.ts Outdated
}: Omit<Job, "sentryBaggage" | "sentryTrace">) {
await instance.enqueue(
{ account, assets, chainId, factory, origin, publicKey, source },
[account, ...(assets ?? [])].join("-"),

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 Include the chain in poke job ids

When the same account receives the same asset on two supported Alchemy networks while the first poke job is still present, both publishes use the same custom job id because this id only includes account and assets. BullMQ deduplicates custom job ids, so the second chain's deploy/poke work is silently dropped; include chainId in the id to keep cross-chain activity independent.

Useful? React with 👍 / 👎.

Comment on lines +19 to +22
await instance.enqueue(
{ account, assets, chainId, factory, origin, publicKey, source },
[account, ...(assets ?? [])].join("-"),
);

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 Match the expected poke publish span name

The new server/test/workers/poke.test.ts test expects startSpan to be called with name: "account poke", but this call leaves the shared queue helper's spanName argument unset, so it emits the default "poke" span instead. As written, the added worker test fails until this enqueue call passes the intended span name or the assertion is updated.

Useful? React with 👍 / 👎.

Comment thread server/workers/poke/worker.ts Outdated
const wallet = createWallet(poker, chain);
const isDeployed = !!(await wallet.getCode({ address: job.data.account }));
span.setAttribute("exa.new", !isDeployed);
if (!isDeployed) {

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 Avoid deploying unfunded accounts from allow jobs

When a Persona approval enqueues an allow job, it reaches this worker with origin: "allow" and no assets; if that KYC-approved account has not received funds, this block still deploys the account before the balance scan below can determine that poked remains false. That makes every unfunded KYC approval spend the poker signer’s gas and emits an AccountFunded track event even though no funds were present; check balances first for allow jobs and only deploy when there is something to poke.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cdc276fc-7a05-4655-bace-fdabd3bdfb4f

📥 Commits

Reviewing files that changed from the base of the PR and between 597de4d and 6721df8.

📒 Files selected for processing (3)
  • server/test/hooks/persona.test.ts
  • server/test/mocks/deployments.ts
  • server/test/workers/poke.test.ts

Comment on lines +75 to +122
async function jobFinished(
current: Parameters<ReturnType<typeof createPoke>["enqueue"]>[0],
options?: JobsOptions,
trace?: Pick<Poke, "sentryBaggage" | "sentryTrace">,
) {
const id = [current.account, ...(current.assets ?? [])].join("-");
const job = await queue.add(
"poke",
{ ...current, ...trace },
{ attempts: 1, jobId: id, removeOnComplete: true, removeOnFail: true, ...options },
);
await job.waitUntilFinished(events).catch(async (error: unknown) => {
await vi.waitUntil(() => vi.mocked(captureException).mock.calls.length > 0);
throw error;
});
return job;
}

async function spyScopeSetUser() {
const { withScope: realWithScope } = await vi.importActual<typeof sentry>("@sentry/node");
const setUser = vi.fn();
vi.mocked(withScope).mockImplementation((_scopeOrCallback, _callback?) =>
realWithScope((scope) => {
const originalSetUser = scope.setUser.bind(scope);
scope.setUser = (...args: Parameters<typeof scope.setUser>) => {
setUser(...args);
return originalSetUser(...args);
};
return ((_callback ?? _scopeOrCallback) as NonNullable<typeof _callback>)(scope);
}),
);
return setUser;
}

async function spySpanSetAttribute() {
const { startSpan: realStartSpan } = await vi.importActual<typeof sentry>("@sentry/node");
const setAttribute = vi.fn();
vi.mocked(startSpan).mockImplementation(((options, callback) =>
realStartSpan(options, (span) => {
const originalSetAttribute = span.setAttribute.bind(span);
span.setAttribute = (...args: Parameters<typeof span.setAttribute>) => {
setAttribute(...args);
return originalSetAttribute(...args);
};
return callback(span);
})) as typeof startSpan);
return setAttribute;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move the standalone helpers to the bottom of the file.

jobFinished, spyScopeSetUser, and spySpanSetAttribute are supporting details. The coding guidelines place standalone function declarations at the bottom, next to internal constants and types. server/test/hooks/persona.test.ts already follows this pattern with postInquiry at Line 2005. Function declarations are hoisted, so the move is safe.

As per coding guidelines: "place the default export (the thing the file exists for) at the top. standalone function declarations are supporting details and belong at the bottom alongside internal constants and types."

Source: Coding guidelines

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

Copy link
Copy Markdown

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: 6721df821e

ℹ️ 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/workers/poke/worker.ts Outdated
Comment on lines +166 to +168
if (job.data.origin === "activity") {
await credit.enqueue(job.data.account, `poke-${job.id}`);
}

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 Re-evaluate credit after allow-origin pokes

When an already-created KYC credential has an active debit-mode card and funds were waiting behind the firewall, the allow job can successfully poke those balances, but this condition excludes origin: "allow" from enqueueing the credit worker. The original inbound activity may already have exhausted its retries before permission was granted, so no later event is guaranteed to evaluate automatic credit and the eligible card remains in debit mode until another deposit or manual action.

Useful? React with 👍 / 👎.

Comment thread server/workers/poke/worker.ts Outdated
.catch((error: unknown) => captureException(error, { level: "error" }));
}
if (job.data.origin === "activity") {
await credit.enqueue(job.data.account, `poke-${job.id}`);

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 Make retained credit IDs unique per poke execution

When a later activity creates a new poke job for the same account and assets while the previous credit job is still among the 100 retained completions, the poke queue reuses the same job.id, making poke-${job.id} collide with the completed credit job. BullMQ then returns that existing job instead of running another credit evaluation, so changes such as removing a USDC deposit and receiving more of the same collateral can leave automatic credit disabled until the retained job is evicted.

Useful? React with 👍 / 👎.

Comment thread server/workers/poke/worker.ts Outdated
Comment on lines +140 to +142
if (job.data.origin === "activity") {
await job.updateData({
...job.data,

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 Recover successful pokes when checkpointing fails

If this Redis update fails, or the worker exits after the on-chain transaction succeeds but before this checkpoint is persisted, the retry still contains the asset that was already poked. Its wallet balance is then zero because the funds are in the market, so the retry classifies it as pending, repeatedly throws NoBalance(), and never reaches the credit enqueue; a transient checkpoint failure can therefore turn a successful deposit into a terminal job without automatic credit processing.

Useful? React with 👍 / 👎.

Comment thread infra/utils/modules.ts
workers: {
allow: { signers: ["allower"] },
credit: { secrets: ["onesignal-api-key", "postgres-url"] },
poke: { secrets: ["onesignal-api-key", "segment-write-key"], signers: ["poker"] },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use an authorized keeper for poke transactions

In an infra-deployed worker this creates and loads a distinct poker KMS account, but the deployed plugin grants KEEPER_ROLE only to acct("keeper") in contracts/script/ExaPlugin.s.sol, while poke and pokeETH are explicitly mapped to keeper runtime validation in contracts/src/ExaPlugin.sol; a repo-wide search finds no role grant for the new poker address. Consequently every main-chain poke sent by this worker reverts Unauthorized and exhausts its retries, so the worker must reuse the authorized keeper or provision an on-chain grant for the KMS address.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: 6d189bff56

ℹ️ 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/workers/poke/queue.ts Outdated
}: Omit<Job, "sentryBaggage" | "sentryTrace">) {
await instance.enqueue(
{ account, assets, chainId, factory, origin, publicKey, source },
[account, ...(assets ?? [])].join("-"),

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 Requeue activity after retained poke failures

When a poke job exhausts its retries, the shared queue retains it for up to seven days (server/workers/queue.ts:22). Because this ID is deterministic for an account and its assets, a later Alchemy event for the same account and asset is treated by BullMQ as the existing failed job instead of starting new work, so deposits made after a temporary outage remain unpoked until cleanup; remove failed poke jobs or use a unique activity-execution ID.

Useful? React with 👍 / 👎.

Comment thread server/workers/poke/worker.ts Outdated
pending.push(asset);
continue;
}
const receipt = await wallet.exaSend(

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 Continue processing assets after one poke fails

When one activity contains multiple supported assets and an earlier asset's poke persistently reverts, this sequential await aborts every attempt at that asset. Since the failed asset remains ahead of the untouched assets in the checkpointed job data, later valid assets are never visited and remain idle after the job exhausts its retries; process the assets independently or split the remaining assets into separate work.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: 1fc82aa002

ℹ️ 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/api/card.ts
.insert(cards)
.values([{ id: card.id, credentialId, lastFour: card.last4, mode, productId }]);
await database.insert(cards).values([{ id: card.id, credentialId, lastFour: card.last4, productId }]);
await credit.enqueue(account).catch((error: unknown) =>

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 Preserve credit work when enqueueing fails

When an already-funded account creates its card while Redis or BullMQ is temporarily unavailable, this catch returns success after storing the card in debit mode but discards the only credit evaluation scheduled by card creation. The earlier deposit's activity job may have completed before the card existed, and no later event is guaranteed to enqueue another evaluation, so an otherwise eligible card can remain in debit mode indefinitely; persist or retry this work without failing card issuance.

Useful? React with 👍 / 👎.

Comment thread server/hooks/activity.ts
captureException(error, { level: "error", fingerprint: revertFingerprint(error) });
});
throw error;
await Promise.all(

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 Defer notifications until queue publication succeeds

When poke.enqueue rejects after the notification promise above has started, this awaited publication returns a 500 even though the user may already have received the funds notification. A normal webhook redelivery then executes the notification path again, so a transient Redis failure can send duplicate push notifications; publish the durable job before sending the notification or otherwise deduplicate that side effect. .agents/rules/server.mdL75-L76

Useful? React with 👍 / 👎.

const card = credential?.cards[0];
span.setAttribute("exa.card", card?.id);
if (card?.mode === 0) {
await database.update(cards).set({ mode: 1 }).where(eq(cards.id, card.id));

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 Make automatic mode activation conditional

Because card creation now stores mode 0 and delegates activation to this asynchronous worker, a concurrent card PATCH can change the mode or delete the card after this worker reads card.mode but before this unconditional update runs. The worker then overwrites the user's selected installment mode—or updates a deleted card—and sends an incorrect activation notification; include the observed mode and active status in the update predicate and notify only when a row was actually changed.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: 1f7457be78

ℹ️ 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 on lines +11 to +12
async enqueue(account: Job["account"], jobId: string = account) {
await instance.enqueue({ account }, jobId);

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 Allow replacement cards to requeue credit checks

When a user replaces a deleted card within seven days after the previous card's automatic-credit job exhausted its retries, this default ID reuses the account address while the failed job is still retained by server/workers/queue.ts. BullMQ therefore treats the replacement card's enqueue as a duplicate, so the new card never gets its collateral evaluated and remains in debit mode until another activity happens to schedule a differently named credit job; remove failed account-scoped jobs or make each card-creation check unique.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: 8f9e6e7895

ℹ️ 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 infra/utils/modules.ts
common: ["redis-url", "sentry-dsn"],
crema: ["redis-address", "redis-password", "redis-username"],
workers: {
allow: { signers: ["allower"] },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Provision firewall authority for the allower signer

In an infra deployment this creates a new allower KMS account, and server/workers/allow/worker.ts uses that EOA to call Firewall.allow directly, but a repo-wide search finds no on-chain role grant or other provisioning for its address. Consequently Persona approvals enqueue transactions from an unauthorized signer, so allow jobs exhaust their retries and approved accounts remain blocked; reuse the already-authorized account or add an explicit firewall grant during deployment.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: bd1003bae7

ℹ️ 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/workers/poke/queue.ts Outdated
}: Omit<Job, "sentryBaggage" | "sentryTrace">) {
await instance.enqueue(
{ account, assets, chainId, factory, origin, publicKey, source },
[account, ...(assets ?? [])].join("-"),

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 Preserve deposits that arrive during an active poke job

When a second deposit of the same asset reaches the same account while the first poke job still exists, this deterministic ID makes BullMQ return the existing job without merging the new activity. If the first worker has already completed its on-chain poke but has not yet finished the job, the second deposit remains in the wallet and its webhook cannot schedule another poke; include an activity-specific identifier or explicitly merge/requeue duplicate activity.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
server/workers/poke/queue.ts (1)

19-22: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add origin to the job ID and normalize asset order.

The job ID is chainId-account-assets. It omits origin. BullMQ ignores an add with an existing jobId instead of replacing the payload. Two producers can therefore collide:

  • server/hooks/activity.ts Line 189 enqueues origin: "activity".
  • server/workers/allow/worker.ts enqueues origin: "allow" with the same account, chainId, and assets.

When both jobs share the same account, chain, and asset list, the second enqueue is dropped. The dropped job's origin-specific behavior in server/workers/poke/worker.ts is then skipped: the "allow" push notification, or the "activity" credit enqueue and pending-asset retry.

Asset order is also not normalized. [weth, usdc] and [usdc, weth] produce different IDs for the same account, so two concurrent jobs poke the same account.

Sort the assets and include origin in the ID.

🐛 Proposed fix
       await instance.enqueue(
         { account, assets, chainId, factory, origin, publicKey, source },
-        [chainId, account, ...(assets ?? [])].join("-"),
+        [chainId, account, origin, ...[...(assets ?? [])].sort()].join("-"),
       );

Note: server/test/workers/poke.test.ts Line 79 and Lines 147, 180, 201, 500, 568, 613 build the expected ID with the same formula. Update them together.

server/hooks/activity.ts (1)

176-199: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add an idempotency key to the push notification.

Line 187 awaits poke.enqueue. A rejection returns 500, so Alchemy retries the webhook delivery. The poke job deduplicates through its job ID, but the notification sent at Line 177 does not. Each retry delivers another "Funds received" message for the same transfer.

Pass a deterministic idempotencyKey to onesignal.sendPushNotification. server/utils/onesignal.ts already forwards it to OneSignal.

🛡️ Proposed fix
-      for (const { toAddress: account, rawContract, value, asset: assetSymbol } of transfers) {
+      for (const { toAddress: account, hash, rawContract, value, asset: assetSymbol } of transfers) {
         if (!accounts[account]) continue;
         if (chain.id === exaChain.id && rawContract?.address && markets.has(rawContract.address)) continue;
         const asset = rawContract?.address ?? ETH;
         const underlying = asset === ETH ? WETH : asset;
         const notification = {
           userId: account,
+          idempotencyKey: `${hash}-${asset}`,
           headings: t("Funds received"),
server/test/workers/poke.test.ts (1)

74-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the standalone helpers to the bottom of the file.

jobFinished, spyScopeSetUser, and spySpanSetAttribute are supporting details. createRequest, getMarket, and mint already sit at the bottom of this file. Move these three next to them. Function declarations are hoisted, so the move is safe.

As per coding guidelines: "place the default export (the thing the file exists for) at the top. standalone function declarations are supporting details and belong at the bottom alongside internal constants and types."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c335e9ea-3755-4404-b17f-7937b8be58c4

📥 Commits

Reviewing files that changed from the base of the PR and between 86fbfe2 and 9309627.

📒 Files selected for processing (8)
  • infra/utils/modules.ts
  • server/hooks/activity.ts
  • server/index.ts
  • server/test/e2e.ts
  • server/test/hooks/activity.test.ts
  • server/test/workers/bin.test.ts
  • server/test/workers/poke.test.ts
  • server/workers/poke/queue.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

cruzdanilo and others added 3 commits August 31, 2026 18:48
co-authored-by: Miguel Diaz <github.com.hf06j@slmail.me>
co-authored-by: Miguel Diaz <github.com.hf06j@slmail.me>
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: poke account after allowing it in the firewall

1 participant