Investments

The full lifecycle of a forex investment — what the create route checks, what the hourly cron pays out and how it computes it, the four statuses, refunds on cancellation, and recovering one that failed.

9 min readUpdated 3 August 2026investments, settlement, cron, refunds, lifecycle

An investment is a principal, a plan, a duration and an end date. It has four statuses and exactly two ways out: it settles, or it is cancelled and refunded. Nothing else may take money off it.

Opening one

The customer opens /forex/plan, picks a plan, picks one of the durations that plan offers, enters an amount and accepts the terms. Before a row is written, the request passes eight checks in order:

  1. KYC — the customer's verification level must carry invest_forex.

  2. Terms accepted — the request is refused without it, and the acceptance timestamp and version are stored on the investment.

  3. The plan is enabled — a plan an admin has switched off is refused with This plan is not currently accepting investments. Its existing investments continue to settle.

  4. The amount is a positive, finite number — checked before the limits, because the limit comparison is skipped on a plan whose minimum is zero. A negative amount used to pass every check and credit the forex account, which let anyone with a live account mint withdrawable money.

  5. The amount is within the plan's limitsminAmount and maxAmount.

  6. The duration is offered by this plan — the forex_plan_duration join is consulted. A duration that exists but is not attached to this plan is refused.

  7. Fraud checks — see below.

  8. The forex account can pay — inside a transaction, with the account row locked: it must be a LIVE account, its currency and wallet type must match the plan's, and its balance must cover the principal.

Only then is the row created, the principal debited, and an audit transaction of type FOREX_INVESTMENT written against the customer's matching wallet. The investment starts ACTIVE with its end date already computed.

The creation endpoint is rate-limited to 10 investments per hour per caller.

The fraud checks

Two hard limits, neither of them configurable:

Check Refuses
More than 10 ACTIVE investments already held Too many active investments
Amount above the plan's own maxAmount Investment amount exceeds this plan's maximum

The second is worth noting because it used to be a hidden literal of 50,000 that overruled the plan: a plan an admin had deliberately configured with a maximum of 100,000 rejected a legitimate 60,000 investment, with a security-sounding message that gave no hint the admin's own setting had been overridden.

If a fraud check itself errors — a database problem, a missing price — the request is allowed. The checks are a filter, not a gate.

The four statuses

Status Means Holds the principal?
ACTIVE Running. The principal has left the customer's forex account Yes
COMPLETED Settled. The result was paid into the forex account No
CANCELLED Abandoned. The principal was refunded No
REJECTED Same as cancelled, with a different label No

Only ACTIVE is a state where money is at stake. Everything that moves an investment out of ACTIVE without settling it has to hand the principal back, and every admin path that does so goes through one shared transition that performs the refund in the same transaction as the status change.

Settlement

The hourly processForexInvestments cron does all of it. Each run loads every ACTIVE investment with its plan and duration, and for each one whose stored end date has passed:

  1. Lock the row and re-check. An investment that is no longer ACTIVE is skipped. This is what makes a manual cron trigger racing the scheduled tick safe.

  2. Find the customer's LIVE forex account and lock it. If there is none, nothing is paid and the investment is left ACTIVE for manual review.

  3. Compute the movement. principal × profitPercentage ÷ 100, taking the percentage from the plan the customer was quoted.

  4. Decide the result. The investment's own result column if an admin set one, otherwise the plan's default result.

  5. Flip the status to COMPLETED, conditionally on it still being ACTIVE. That single conditional update is what makes the payout idempotent — if another run already settled it, zero rows match and nothing is paid.

  6. Credit the forex account.

    • WIN — principal plus the profit
    • LOSS — principal minus the loss, floored at zero
    • DRAW — the principal
  7. Record the platform's side — the payout on a win, the retained principal on a loss — so the addon's contribution to profit and loss is visible per currency. This step is best-effort by design: a problem writing it can never strand a settled investment or hold up the customer's payout.

  8. Pay any affiliate reward, then email and notify. The affiliate credit runs first and in its own error handler, because it is a financial credit to a third party and must not be skipped because an email failed.

A loss is written as a negative number. Rows settled before that was true hold the loss as a positive number beside a LOSS result, which is why every reporting surface in the addon applies a sign correction when it sums the column. If you query forex_investment.profit directly, apply the same rule: a LOSS row's profit is the absolute size of the loss, negated.

Payouts go to the forex account

Not to the spot wallet. The principal left the forex account, so the return belongs there, and the customer takes it out through the normal withdrawal queue like any other forex balance.

This matters operationally: a matured investment is not money the customer has received. It is money sitting in a sub-account waiting on your approval. A busy settlement day produces a busy withdrawal queue the following morning.

When settlement fails

Each investment gets up to three attempts with exponential backoff. If all three fail, the cron cancels the investment and refunds the principal in the same transaction, records how much it refunded in the investment's metadata, and notifies the customer. If a safe refund is impossible — there is no LIVE account to return the money to — the investment is left ACTIVE and an alert is logged rather than being cancelled with the money stranded.

Two deterministic failures are handled separately and never enter the retry path: an investment whose plan or duration cannot be resolved (usually a soft-deleted plan) is logged and left ACTIVE for manual resolution, because retrying it three times and then cancelling would be worse than leaving it alone.

Admin actions

Admin → Forex → Investments (/admin/forex/investment).

Editing

Every field on the edit dialog saves as before except the amount, which is fixed once the investment exists.

The principal has already been taken from the customer's forex account. Changing the amount would change what the investment pays at maturity without changing what was collected — raising a 1,000 investment to 10,000 returned 10,000 against the 1,000 that had actually been paid in, and lowering it did the reverse and kept the difference.

The route refuses the change and tells you what to do instead: cancel the investment, which refunds the principal, and create a new one.

Setting result on an individual investment before it settles is how you make one investment differ from the plan's default outcome.

Cancelling

Three doors set the status — the single status endpoint, the edit dialog and the bulk selector — and all three now go through the same refunding transition. Only one of them used to refund; the other two stranded the customer's money.

The transition is safe to repeat: the status flip is conditional on the status you read, so a second click is a no-op rather than a second refund. It refunds only when the investment was ACTIVE, because one that is already cancelled was refunded when it got there and a completed one has been paid out.

If the customer has no LIVE forex account to refund to, the cancellation is refused rather than silently discarding the principal.

Deleting

Deleting an ACTIVE investment is refused, and the message tells you how much principal the selection is holding. Deletion would take the investment out of the settlement cron's reach and the money would never come back. Cancel first — that refunds — then delete if you want the row gone.

Recovering

CANCELLED investments can be re-armed with Recover, which needs edit.forex.investment.

A cancelled investment has almost always already been made whole — the cron's terminal-failure path refunds the principal and records the amount in the investment's metadata. Re-arming it and settling it again would pay the principal twice: once as the refund, once as the maturity payout.

Recovery therefore takes the refund back out of the customer's forex account before flipping the investment to ACTIVE. If the account can no longer cover it, recovery is refused with the two figures named, and you are told to have the customer restore the balance or leave the investment cancelled.

Two admins clicking Recover at the same time cannot both reclaim: the flip is conditional on the investment still being CANCELLED.

Creating from the admin side

There is no Create button on the investments table, but the endpoint exists and behaves exactly like the customer path: it locates the customer's LIVE forex account, checks the denomination against the plan, and debits the principal before writing the row.

That last part is why the button is worth avoiding unless you know what you are doing. The route used to write the row and touch no balance at all, so an investment created with a past end date paid out both principal and profit on the next cron tick with nothing having been paid in — anyone holding create.forex.investment could mint spendable balance.

What the customer sees

/forex/investment lists their investments; /forex/investment/{id} shows one in full with its plan, duration and result. /forex/dashboard carries totals, a plan distribution and recent activity. All of it is scoped to the caller — there is no way to read another customer's investments.

An investment that has not settled shows no profit or loss, rather than +0.00 in green.