ICO troubleshooting

Offerings that never settle, contributions stuck in escrow, every refund door and what it refuses, phase supply that will not reconcile, and the settings key that does nothing.

14 min readUpdated 6 August 2026troubleshooting, escrow, refunds, cron, diagnostics

Every ICO fault is a money fault. The platform holds the payment currency of every live contribution on one wallet, and nothing releases it without either a cron run or an administrator clicking something. An offering that does not settle is not a cosmetic bug — it is contributors' money held indefinitely.

Backend log modules to filter on:

Module Written by
ICO_OFFERING_PROCESS The hourly settlement cron's own wrapper
ICO Settlement itself, including per-offering failures and refund totals
ICO_VESTING The vesting cron
ICO_INVESTMENT The purchase route
ICO_REFUND The shared refund authority and /api/ico/refund/process
ICO_RELEASE A creator submitting a delivery hash
ADMIN_ICO The admin offer, transaction and settings routes
ADMIN_ICO_CANCEL The Super Admin emergency cancel

First checks

Before anything specific:

    • The ICO extension row is active at /admin/system/extension
    • Process ICO Phases and Process ICO Vesting both appear at /admin/system/cron with a recent, green run
    • One account holds the Super Admin role — it is the escrow
    • The offering's launch plan currency and wallet type still exist as a real wallet type on your install

The first two cover most of this page. The third causes purchases to fail with what reads like a server fault; see Install.

Nothing settles without the cron

Two jobs, both hourly, both in the cron process — the PM2 app started with CRON_MODE=only on port 4001.

Job on screen Function What it decides
Process ICO Phases processIcoOfferings Promotes UPCOMINGACTIVE, then closes ended offerings as SUCCESS or FAILED and refunds every contribution on a failure
Process ICO Vesting processIcoVestingReleases Announces tranches that have come due, closes finished schedules, cancels schedules behind refunded contributions

processIcoOfferings is the only automatic path that ends a sale and the only automatic path that pays contributors back when it fails. With the cron process down, an ended offering sits at ACTIVE forever: it refuses new contributions because its end date has passed, it never becomes FAILED, and because the refund doors accept only FAILED and CANCELLED offerings, there is no way to hand the money back short of cancelling the sale.

Neither job has a manual equivalent inside the ICO screens. If you need one to run now, use Run now on its row at Admin → System → Cron.

An offering is stuck UPCOMING past its start date

UPCOMING is promoted to ACTIVE by the cron, in two places within the same job: an early pass in processIcoOfferings, and again inside the settlement stage it delegates to. Either one moves it. If neither has, the job is not running — check the run state on the cron screen before looking anywhere else.

Approving an offering sets it to ACTIVE immediately, whatever its start date; the purchase route is what refuses early contributions, with "Offering has not started yet". So an UPCOMING row on a current install came from a direct database edit or from a much older build. It is still promoted correctly — but do not wait for an "Upcoming" queue to fill itself.

An offering is stuck ACTIVE past its end date

This is the one that matters. The early pass in processIcoOfferings deliberately does not decide the outcome — it logs "Offering X has ended - status will be determined by checkAndUpdateOfferingStatus" and moves on. The whole end-of-sale decision, including SUCCESS versus FAILED and every refund, happens in the settlement stage that runs after that loop.

So an offering frozen at ACTIVE past its end date means one of exactly two things:

  1. The job is not running at all.
  2. The job runs, but the settlement stage is failing — for this offering or for the whole stage.

Tell them apart on the cron screen. A job that is not running has a stale Last run. A job that is running and failing shows red with a Last error.

A red run on Admin → System → Cron

Settlement runs one database transaction per offering, on purpose: refunding a large failed sale takes row locks on every contribution and on the escrow wallet, which is the hottest row on the platform. One sale's failure therefore does not stop the sweep — the offering is rolled back, the error is collected, and the loop continues to the next one. At the end, if anything was collected, the whole run throws and is reported failed.

That means a red run does not mean nothing settled. Open the job on Admin → System → Cron and read Last error; it names the offerings:

ICO offering status update: 2 offering(s) failed: offering <uuid>: <message>; offering <uuid>: <message>

Only the first ten are listed. The full set is in the log under ICO, one Error settling offering <id> line each.

Two nuances worth knowing:

  • Errors raised while promoting UPCOMING offerings in the early pass are logged under ICO_OFFERING_PROCESS and broadcast to the run log, but they are not collected — they do not turn the run red on their own.
  • If the settlement stage fails as a whole rather than per offering, the run is reported failed with Phase manager update failed: …. It is rethrown deliberately: swallowing it once let the panel report "completed, 100% success" every hour while no ended offering settled and no contributor was refunded.

Rerun with Run now once you have fixed the cause. Settlement is safe to repeat — an offering that already moved off ACTIVE is no longer selected, and every refund is idempotent against the ledger.

A contribution is stuck

Read the status first. Two of the five look like problems and only one of them is yours.

Status What it means Who owes the next move Where the money is
PENDING Paid. Creator has not delivered tokens The creator Escrow
VERIFICATION Creator submitted a delivery hash You Escrow
RELEASED You verified it Nobody Paid to the creator
REJECTED You refused the delivery Nobody Refunded to the buyer
REFUNDED Swept back by a failure, a cancel or a refund run Nobody Refunded to the buyer

Both sit on the Super Admin escrow wallet. The buyer has paid in both cases and the creator has been paid in neither. The only difference is who is late.

A PENDING contribution is a creator problem — chase the project, or refuse to release escrow. A VERIFICATION contribution is an operator problem: it is on your desk at Admin → ICO → Transactions, and while it waits the creator is not being paid for tokens they have already sent.

A vesting contribution stays PENDING longer than operators expect. Its whole-amount release door is closed — a creator who tries it is refused with "This contribution vests. Record each tranche on the Vesting tab as you send it…" — and the contribution moves to VERIFICATION only when the first tranche is recorded. Verifying that first tranche releases the entire purchase amount; there is no partial escrow release. See Vesting.

If the status is right but the ledger looks wrong, work through Reconciling a stuck contribution.

Refund paths, and what each one refuses

There are four doors into one shared refund implementation. They differ only in who may open them and which offerings they accept.

Door Who Accepts UI
Reject a delivery edit.ico.transaction One contribution, VERIFICATION only Transaction desk
Soft-cap failure The cron Every outstanding contribution on an offering it just marked FAILED None — automatic
POST /api/ico/refund/process The offering's own creator, or any Admin / Super Admin Offerings in FAILED or CANCELLED only None
POST /api/admin/ico/offer/[id]/cancel-refund Super Admin role only Any offering; sets it CANCELLED first Offering detail page
Refund every outstanding contribution on a failed or cancelled offering
Check whether an offering is eligible for refunds
Cancel a live offering and refund every contributor

Three things trip operators up here.

The refund run has no screen. POST /api/ico/refund/process is API-only — nothing in the admin panel calls it. It takes offeringId and a reason, both required:

curl -X POST "https://your-host/api/ico/refund/process" \
  -H "Content-Type: application/json" \
  -H "Cookie: accessToken=..." \
  -d '{"offeringId":"OFFERING_ID","reason":"Soft cap not reached"}'

It refuses anything that is not FAILED or CANCELLED with Cannot process refunds for offering with status: ACTIVE. You cannot hand money back on a running sale — pause it, let it end, or cancel it. It also refuses with Every contribution on this offering has already been refunded when there was nothing left to pay, which is a success, not a fault.

The emergency cancel is gated on the role name, not on a permission. The route declares manage.system, and then additionally requires the caller's role to be literally Super Admin. Granting manage.system to a custom role does not open it. It needs a reason of at least ten characters, which goes into the audit trail.

GET /api/ico/refund/eligibility/[offeringId] computes its soft cap as targetAmount × 0.3 regardless of what icoSoftCapPercentage is set to, and it counts REFUNDED contributions into the totalRaised it reports. Settlement uses neither of those rules.

If you have moved the soft cap off 30, treat this endpoint's eligible and softCap as advisory. The number that decides SUCCESS versus FAILED is the one on Admin → ICO → Settings → Investment.

Nobody is refunded twice, whichever door is used. All four share one idempotency rule and the ledger is the authority: one completed REFUND against a contribution id means that buyer has their money, and every later sweep skips the row rather than paying again.

Contributions left REJECTED by the old lifecycle

An older build's failure sweep flipped every live contribution on a failed sale to REJECTED and stopped there. Nothing scheduled ever refunded those rows, and all three manual doors accepted only PENDING and VERIFICATION — so the status change itself put the money out of reach of every remaining path. Investors were left holding a rejected transaction and no way to get their contribution back.

REJECTED is now a refundable status. But nothing sweeps it on a timer: it is paid out only when a refund is explicitly triggered against the offering.

The transaction desk's status filter offers PENDING, VERIFICATION, RELEASED and REJECTED — and REJECTED there mixes legitimate admin rejections, which were already refunded, with the stranded ones, which were not. REFUNDED is not a filter option at all. You have to go to the database.

List them first. This finds REJECTED contributions with no completed refund leg on the ledger, using the same test the refund authority itself applies:

SELECT o.id     AS offeringId,
       o.name,
       o.status AS offeringStatus,
       t.id     AS contributionId,
       t.userId,
       t.amount * t.price AS value,
       o.purchaseWalletCurrency AS currency
FROM ico_transaction t
JOIN ico_token_offering o ON o.id = t.offeringId
LEFT JOIN `transaction` r
       ON r.referenceId IN (CONCAT(t.id, '_refund'), t.id)
      AND r.type = 'REFUND'
      AND r.status <> 'FAILED'
      AND (r.description NOT LIKE '%escrow%' OR r.description IS NULL)
WHERE t.status = 'REJECTED'
  AND t.deletedAt IS NULL
  AND r.id IS NULL
ORDER BY o.name;

Rows that come back have been paid for and never paid back. Then work per offering:

  1. Check the offering's status. The refund run accepts only FAILED or CANCELLED. A stranded row on an offering still sitting at ACTIVE past its end date is a settlement problem first — fix the cron and let it settle.

  2. Run the refund with POST /api/ico/refund/process for that offering, as above. Contributions already refunded through the rejection door are skipped by the idempotency check, so the run pays only the stranded ones.

  3. Read the response. refundedCount and totalRefunded are what moved; failedRefunds is an array naming each contribution that could not be paid and why. The commonest reason is a wallet that could not be resolved.

  4. Re-run the query. Anything still listed failed, and the reason is in the response and in the log under ICO_REFUND.

Refunds are sourced from the escrow wallet before the buyer is credited. If no Super Admin exists, the credit still happens and is logged as unbalanced — the buyer is made whole out of the platform's own funds. Fix the Super Admin; do not rely on that.

icoRefundGracePeriod does nothing

PUT /api/admin/ico/settings/limits accepts refundGracePeriod and writes icoRefundGracePeriod; the matching GET reads it back with a default of 7. Nothing in the refund logic consults it anywhere.

There is no grace period, no window after which a refund is refused, and no automatic refund triggered by its expiry. Refund eligibility is decided entirely by the offering's status. The key is not on the ICO settings screen either — the only way to set it is that endpoint.

Both limits endpoints additionally gate on the caller's role name and accept only Admin or Super Admin, so a custom role holding view.ico.settings is refused there even though the settings page itself opens for them.

Phase remaining does not match what was sold

Every purchase decrements remaining on the phase that priced it, and every refund, rejection and failure sweep puts the tokens back into that same phase — the contribution records its phaseId. Contributions written before that column was populated fall back to the offering's first phase on refund, which is the one way the two figures can legitimately drift apart.

Reconcile per offering:

SELECT p.name,
       p.allocation,
       p.remaining,
       p.allocation - p.remaining AS soldByPhase,
       COALESCE(SUM(t.amount), 0) AS soldByContributions
FROM ico_token_offering_phase p
LEFT JOIN ico_transaction t
       ON t.phaseId = p.id
      AND t.deletedAt IS NULL
      AND t.status IN ('PENDING', 'VERIFICATION', 'RELEASED')
WHERE p.offeringId = 'OFFERING_ID'
GROUP BY p.id, p.name, p.allocation, p.remaining, p.sequence
ORDER BY p.sequence;

soldByPhase above soldByContributions means allocation is burnt — tokens that can never be sold to anybody. Below it means supply was credited back twice. Either way, correct remaining directly; there is no admin screen that sets it.

Remember which phase is selling: the lowest sequence with remaining > 0, not the phase whose date window contains today. Durations are display only. That is why a phase can sell out on day three and hand over immediately, and why a phase whose window closed keeps selling.

A sale stopped taking money and you cannot see why

Two caps end a sale and they are measured in different units, so either can bite while the other looks healthy.

Refusal message Cap that fired
No tokens available for sale Every phase has remaining = 0
Only N tokens remaining in current phase The current phase's remaining, for this one contribution
Investment exceeds target amount. Only N ... remaining. targetAmount, against the sum of every PENDING/VERIFICATION/RELEASED contribution

Nothing checks the two against each other at creation time. Multiply out allocation × tokenPrice for every phase and compare the total with targetAmount — see Phases and pricing. A contribution never spills over from one phase into the next at the next price; the buyer is refused and has to buy twice.

An offering with no phase left stays ACTIVE until its end date and then settles on whatever it raised. That is not a fault, but it means the progress bar can sit at 30% with the sale genuinely over.

Soft-cap failure refunded a sale you thought succeeded

At settlement, total raised is compared with targetAmount × icoSoftCapPercentage. At or above it the offering becomes SUCCESS; below it, FAILED — and a FAILED offering refunds every outstanding contribution in the same pass.

Share of an offering's target it must raise to count as funded

Things to check when the outcome surprises you:

  • The setting is read at settlement time, so a change applies to offerings already running. Raising it can turn a sale that would have succeeded into one that fails.
  • A value outside 0–100, or an unparseable one, silently falls back to 30%. If someone set it to 0.3 meaning "30 percent", settlement used 0.3% — every sale succeeded.
  • targetAmount is editable on a live offering. The soft cap is a percentage of whatever the target is at settlement, not at launch.
  • The raised figure counts PENDING too. A contribution is paid the moment it is made. Rejected and refunded rows drop out.

The sweep returns every PENDING, VERIFICATION and REJECTED contribution. A creator who already sent tokens on-chain to a VERIFICATION buyer has sent the tokens and had the money handed back. The platform has no custody of the token and cannot reverse the transfer.

That is correct for a failed raise, but it is worth saying to a creator when you approve a sale whose target looks optimistic.

Evidence was destroyed by an edit to notes

ico_transaction.notes is a TEXT column holding JSON, not free text. It carries the purchase metadata every later screen and every refund path reads: the phase name, the token decimals, the raw token amount, the investment amount, the fee percentage and fee, the currency, the wallet transaction id, and later the delivery hash and the refund record.

Replacing it destroys the purchase record. The creator's release door threw on parsing it and every refund path silently skipped the contribution.

The admin actions are safe — save-note, remove-note, verify and reject all merge into the object rather than assigning over it, and an older plain-text value is preserved as a note key rather than dropped. Hand edits in the database are the exposure. If you are correcting a row, edit the JSON.

Only save-note is refused once a contribution is RELEASED"Cannot add note: Transaction already verified and released." remove-note carries no status check at all: it requires only that a note exists, so an administrative note can still be stripped off a RELEASED contribution, and the buyer and the creator are both emailed and notified when it is.

A buyer's address or a creator's hash is rejected

Both checks are driven by one string: the value column of the row in ico_blockchain, compared lowercase. The creator picks a blockchain by that value at launch, and it is stored on the offering's token detail.

value Address format enforced Hash format enforced
ethereum, bsc, polygon 0x + 40 hex 0x + 64 hex
bitcoin bc1… or legacy 1/3 64 hex
solana 32–44 base58 87–88 base58
anything else 20–128 characters, no whitespace none — any string is accepted

The messages are Invalid <value> wallet address format on a contribution and Invalid <value> transaction hash format on a delivery submission, so the message itself tells you which value the offering is carrying.

Two failure shapes:

Everything is rejected. The row's value is one of the five and the address genuinely does not match — usually the buyer pasted an address for a different chain, or a creator pasted an explorer URL for a chain the extractor does not know. Explorer URLs are unwrapped automatically only for etherscan.io, bscscan.com and polygonscan.com; anywhere else, paste the bare hash.

Nothing is rejected. The row's value is not one of the five — eth, Ethereum Mainnet, ETH with trailing whitespace — so addresses fall back to a length check and hashes are not checked at all. A buyer can typo their receiving address into an unrecoverable transfer.

The chosen value is copied onto the offering's token detail at launch. Correcting ico_blockchain changes what future creators pick; it does not touch a single existing offering.

To fix one, edit the offering itself — PUT /api/admin/ico/offer/[id] with a blockchain field, which is the Edit dialog on the offering detail page. Do it before the first contribution, not after: contributions already taken carry the address they were validated under.

Nothing on this platform ever checks a hash against a chain. Only its format is validated, and only on those five values. Verification is a human decision — open the hash on a block explorer and check the amount and the destination against the contribution before you click. See Transactions and payouts.

Escalation

Collect before you ask for help:

  • The offering id and, if it is one contribution, the contribution id.
  • The offering's status, startDate, endDate and targetAmount.
  • The contribution's status and its notes JSON, verbatim.
  • The Last error text from Process ICO Phases on Admin → System → Cron, and whether the job has run since the problem started.
  • The ICO and ICO_REFUND log lines around the timestamp.
  • The ledger legs present for the contribution — search the platform's transaction table for the contribution id as a reference prefix, not an exact match, because every leg carries its own scoped reference.

That last one settles most escrow arguments on its own: the legs present tell you exactly how far the money got.