Failure Types Reference
All failure states, error types, and recovery actions across Aset entities. Each failure type has corresponding mock data in apps/infra/db/seed.sql.
⚠️ Illustrative — not the code-emitted set
This page was authored as a seed/mock reference, so most failure_type values and error strings in the tables below are illustrative examples, not values the backend actually writes. Do not build FE failure_type switches against them.
Actually emitted by code (V1):
- Deposits — no
failure_typewriter exists. A failed deposit only setsstatus = FAILED(no typed reason). - Redemptions —
INVESTOR_CANCELLED(investor self-cancel; persisted withstatus = REJECTED) and an admin-supplied reason on reject (elseREJECTED). There is no dedicated payout-failurefailure_type. - Yield —
ON_CHAIN_TX_FAILED(on-chain distribute tx failure —yield-distributions.post.create.ts,lib/shared/yield/run-distribution.ts). - Notifications —
INVALID_RECIPIENTandSERVER_ERRORfrom the deliver worker (notifications.worker.deliver.ts), plusBOUNCEDandSPAM_FILTEREDfrom the live SES event path (notifications.webhook.ses.ts). OnlyTIMEOUTandRATE_LIMITEDremain unproduced — nothing writes them today.
Treat the tables below as display-copy guidance for seeded/mock rows, not the runtime taxonomy.
Overview
| Category | Count |
|---|---|
| Deposit States | 5 |
| Redemption Failures | 5 |
| Yield Failures | 4 |
| KYC/SBT States | 3 |
| Notification Failures | 6 |
Deposit Failures deposits.status + failure_type
Handle deposit processing, escrow management, and LP token minting with recovery workflows.
Status flow: PENDING → PROCESSING → COMPLETED or FAILED
| Status | Failure Type | Error Example | Admin Display | Investor Display | Recovery |
|---|---|---|---|---|---|
| FAILED | LP_MINT_FAILED | ERC20: transfer amount exceeds balance | Red "LP Mint Failed" + error | "Your deposit could not be processed" | Admin retries |
| FAILED | FM_NOTIFICATION_FAILED | SMTP delivery failed: mailbox unavailable | Yellow "FM Not Notified" + retry | Not shown (internal) | Admin retries FM notification |
| PROCESSING | — | — | Blue "Processing" + spinner | "Your deposit is being processed" | Auto-escalate if stuck > 1hr |
Redemption Failures redemption_requests.status + failure_type
Workflow with lockup validation and reserve/funding checks. No FM pre-acknowledge step (FM_ACCEPTED removed in v3-34).
Status flow (instant): REQUESTED → PROCESSING → COMPLETED (or PENDING_RESERVE while awaiting partner funds) Status flow (epoch, v3-26): REQUESTED → QUEUED → PARTIALLY_FILLED* → COMPLETED (*partial fills roll over until claimed)
| Status | Failure Type | Error Example | Admin Display | Investor Display | Recovery |
|---|---|---|---|---|---|
| FAILED | PAYOUT_TX_FAILED | Insufficient reserve balance in escrow | Red "Payout Failed" + escrow info | "Redemption could not be completed" | Admin tops up escrow, retries |
| FAILED | FM_NOTIFICATION_FAILED | SMTP connection refused on port 587 | Yellow "FM Not Notified" | Not shown to investor | Retry FM notification |
| FAILED | CONTRACT_EXECUTION_FAILED | execution reverted: InsufficientReserve | Red "Contract Error" + shortfall | "Processing error — contact support" | Admin addresses reserve shortfall |
| FAILED | FUND_TRANSFER_FAILED | ERC20: transfer exceeds allowance — approval expired | Red "Transfer Failed" + deadline | "Fund transfer pending" | FM renews wallet approval |
| REJECTED | LOCKUP_NOT_MET | Lockup period (180 days) has not elapsed | Gray "Rejected" + reason | "Cannot redeem: lockup active" | Wait until lockup expires |
Mock data: Redemption IDs 7–11 in seed.sql.
Yield Distribution Failures yield_distributions.status + failure_type
Automated yield accrual, batch claim processing, and transaction failure handling.
Status flow: PROCESSING → DISTRIBUTED or FAILED
PENDING is gone — the orphan was fixed by deleting its producer (v3-104, migration 0113)
PENDING only ever existed for the sub-second gap between the legacy server-key create path's insert and settleYield (the FM path inserts PROCESSING and stops for POST /{id}/distribute; the indexer only ever writes DISTRIBUTED). A row that stayed there was a crash orphan nothing could heal — the indexer reconciles on tx_hash, which such a row had not been given yet, and yield.scheduler.reconcile only mirrors claimable_yield — so the period read as un-distributed forever with no way to tell from the DB whether holders were paid.
The planned fix was a sweeper. It was superseded: that legacy path was unreachable in product anyway (the admin UI refuses the server key for any fund_wallet pool, fund_wallet is required at create per v3-26, and a display-only pool has no pool_address to distribute from), so the path was deleted instead — POST /yield-distributions 400s without deposit_tx_hash — and migration 0113 made the status unrepresentable: default PROCESSING + CHECK (status <> 'PENDING'). A pre-existing PENDING row with no tx was backfilled to FAILED / ORPHANED_LEGACY_PATH; one carrying a tx would have aborted the migration for manual on-chain reconciliation, since marking a possibly-settled period FAILED records a paid period as unpaid.
⚠️ The yield_status enum keeps PENDING — yield_distribution_investors.status uses it legitimately (an allocation not yet claimed). The ban is a CHECK on yield_distributions only.
A PROCESSING row older than 24h is the state that needs a human
Not a failure type — a stall. The FM's depositYield landed on-chain (the row carries its deposit_tx_hash), so the yield is in the pool, but POST /{id}/distribute was never run and holders have not been paid. Recovery exists: that endpoint, plus the Distribute action on the admin Yield table (offered exactly when there is a deposit tx to verify; disabled with the reason when there is not — a row with no deposit_tx_hash came from the removed server path and is reconciled by the indexer, so retrying it risks paying twice).
Surfaced as dashboard_alert_counts.stalled_yield → the dashboard's "Stalled Distributions" alert, and under the Yield screen's Failed tab as Stalled. The matching ADMIN notification is not built — blocked on copy (v3-104).
| Status | Failure Type | Error Example | Admin Display | Investor Display | Recovery |
|---|---|---|---|---|---|
| FAILED | ORPHANED_LEGACY_PATH | Recorded by the removed legacy server-key create path and never settled (no deposit or settle tx) | Red "Orphaned Legacy Path" | Not shown | Record the period again (nothing reached the chain) |
| FAILED | TX_FAILED | Gas price exceeded maximum threshold | Red "TX Failed" + gas info | "Yield distribution delayed" | Admin retries with higher gas |
| FAILED | FM_NOTIFICATION_FAILED | SendGrid API error 429: rate limit exceeded | Yellow "FM Not Notified" | Not shown | Retry after rate limit resets |
| FAILED | DISTRIBUTION_TX_FAILED | ERC20 transfer failed — pool escrow insufficient | Red "Distribution Failed" + tx hash | "Yield distribution delayed" | Admin tops up escrow |
| FAILED | CLAIM_PROCESSING_FAILED | Batch claim tx gas estimation failed: out of gas for 4 recipients | Red "Claim Failed" + recipient count | "Yield claim pending" | Admin splits batch, retries |
Notes
Batch optimization for gas limits. Auto-retry up to 3x with exponential backoff. SendGrid rate limit: delayed retry (30min–2hr). Mock data: Yield IDs 8–11 in seed.sql.
KYC / SBT Failures users.kyc_status + sbt_status
Identity verification and Soul Bound Token minting with async mint operations.
KYC flow: PENDING → APPROVED / REJECTED
SBT flow: NOT_MINTED → MINTED / FAILED
| Entity | Status | Detail | Admin Display | Investor Display | Recovery |
|---|---|---|---|---|---|
| KYC | REJECTED | Failed identity verification | Red "KYC Rejected" + reason | "Verification unsuccessful — resubmit" | User resubmits KYC |
| KYC | PENDING | Awaiting review | Yellow "Pending Review" | "Under review" | Admin reviews |
| SBT | FAILED | TX timeout or chain error | Red "SBT Mint Failed" + tx hash | "Credential issuance failed" + retry | Admin triggers re-mint |
Notification Failures notification_deliveries.status + .failure_type
⚠️ notification_logs no longer exists. Migration 0110 split it into three tables (v3-103); a delivery outcome now lives on notification_deliveries — status (the delivery_status enum) plus an optional failure_type. See 11-db-schema → Notifications and 13-operations → Data model.
Status flow: PENDING → SENT → (SES event) BOUNCED / COMPLAINED, or FAILED on a send error, or SKIPPED when we deliberately did not send.
SKIPPED is the one that is not a failure and must not be styled as one — it records "we chose not to email this person, and why" (OPTED_OUT / NO_ADDRESS / SUPPRESSED_ADDRESS). Only FAILED counts toward the failed-notification KPI; INVALID_RECIPIENT is excluded from it too (0107).
| Status | Live? | Written by | Meaning | Auto-retry | Recovery |
|---|---|---|---|---|---|
SENT | ✅ | notifications.worker.deliver.ts | Accepted by SES | — | — |
FAILED | ✅ | notifications.worker.deliver.ts | Send threw (SERVER_ERROR, INVALID_RECIPIENT) | SQS 1/5/15/60/240m → DLQ | Fix config / recipient address |
BOUNCED | ✅ | notifications.webhook.ses.ts | SES bounce; a hard bounce also suppresses the address (HARD_BOUNCE) | No | Update the address, then un-suppress |
COMPLAINED | ✅ | notifications.webhook.ses.ts | Recipient marked it spam; suppresses the address (COMPLAINT) | No | Do not re-add without consent |
SKIPPED | ✅ | preferences/decide.ts | Deliberate non-send, reason recorded | — | None needed |
PENDING | ✅ | on fan-out | Queued, not yet attempted | — | — |
failure_type values that are still unproduced. TIMEOUT and RATE_LIMITED exist in notification_failure_type but nothing writes them. SPAM_FILTERED is written now — the SES complaint path maps to it. So of the original six, four are live and two remain reserved.
Mock data: SBT mint failure, LP mint failure, FM bounce in seed.sql.