Skip to content

Job catalogue

Every async job runs through the same BullMQ queue (scani-jobs), consumed by apps/backend/worker. Wire names live in packages/business/jobs/src/job-names.ts; descriptors in packages/business/jobs/src/scheduled-jobs/ (for repeatable jobs) or packages/business/jobs/src/user-jobs/ (for user-initiated jobs); processors in apps/backend/worker/src/processors/.

Scheduled jobs use the advisory-lock wrapper — two overlapping fires of the same name silently no-op rather than race.

NameFrequencyPurpose
pricingHourly (0 * * * *)Refresh current prices for every token referenced by an active holding.
wallet-balancesHourlyRe-sync on-chain wallet balances + transactions across Etherscan, Helius, Bitcoin, Tron, TON.
exchange-balancesHourlyRe-sync exchange holdings + recent trades for every connected exchange integration.
apy-payoutsDaily, 00:00 UTCApply accrued interest to holdings with an APY config due for payout.
historical-price-backfillNightly, 03:00 UTCFill daily-granularity price history for tokens with holdings; respects unpriceableUntil cooldown.
forex-backfillNightly, 03:30 UTCFill historical FX pairs (via Frankfurter) needed by the rollup.
portfolio-value-rollupNightly, 04:00 UTCRecompute portfolio_value_daily for every user at user / institution / account / holding scope.
transfer-linkingNightly, 03:45 UTCPair CEX withdrawals with wallet deposits via LinkTransferPairsUseCase.
backfill-token-identityWeekly, Sunday 02:00 UTCRe-enrich tokens whose providerMetadata hasn’t been touched lately.
reconcile-pending-credentialsEvery minuteSweep stuck pending integration-credential rows (UI flow interruptions).
reconcile-orphaned-user-jobsEvery minuteSweep stuck running user-job rows whose worker process died.
dlq-depth-probeEvery 5 minutesRead the dead-letter queue depth; emit a warn log when it crosses thresholds.
job-heartbeat-probeEvery 10 minutesDetect jobs whose heartbeat went silent; mark them stuck.
hide-closed-holdingsNightly, 04:30 UTCAuto-hide holdings that have been at zero balance for the configured window.

Enqueued by the api in response to a user action. They use a stable per-user job ID so the user can see “in flight” status in the SPA.

NameTriggered byPurpose
screenshot-parseUpload a screenshotSend to OpenAI Vision; materialise the extracted holdings under a manual institution.
exchange-importConnect an exchangeFirst-time backfill: sync balances + transactions; create accounts/holdings.
wallet-importAdd a walletFirst-time backfill: scan the address across the chain; create holdings.
file-importUpload a CSV / fileParse and ingest.
holding-price-updateUser edits a private-token pricePersist the new price + audit row in token_price_edit_history.
refresh-account-balanceUser triggers a manual syncForce-refresh one account’s balances + transactions.
manual-holdings-createUser creates a manual holdingInsert under the manual institution; seed observation.
portfolio-history-backfillAfter import / manual editRebuild portfolio_value_daily for the affected date range for one user.
transaction-import(Reserved)One-off transaction-only import flow.
user-data-deleteUser requests account / data deletionDelete (or export, depending on the flag) all user data per GDPR-style flow.

Defined in packages/business/jobs/src/retry-policies.ts:

PolicyShapeDefault for
standard5 attempts, exponential backoff, 60s base.Most scheduled jobs.
aggressive10 attempts, exponential, 5s base.Reconcilers (reconcile-pending-credentials, reconcile-orphaned-user-jobs).
none1 attempt.Probes (dlq-depth-probe, job-heartbeat-probe).
user-import3 attempts, longer base.User-import jobs — fail fast so the user can re-try.

Jobs that exhaust their retries land in scani-dlq. The dlq-depth-probe job alarms when depth grows. Operators replay via the HMAC-gated jobs.dlqReplay endpoint on the api.

See Adding a scheduled job for the three-place change required.