General Investment: enable and configure
The switches, KYC feature, cron job, permission keys and menu entries behind the core investment product at /investment — and how to tell it apart from the AI Investments addon.
General Investment is a money-moving earn product that ships with core. A
customer picks a plan, picks a term, hands you the principal from their wallet,
and an hourly job pays them out when the term ends. It has its own route
(/investment), its own admin screens under Finance → Investment
Management, twenty-four admin endpoints, eight customer endpoints, its own
settlement cron and fifteen permission keys.
Nothing seeds its master switch, so on a fresh install it is off and every purchase is refused. This page is the checklist for turning it on properly, or off completely.
/admin/ai/investment is a different product. AI Investments has its own
tables (ai_investment, ai_investment_plan, ai_investment_duration), its own
routes under (ext)/ai/…, its own settlement cron and its own permission keys
(*.ai.investment.*). Nothing is shared. A plan created there never appears at
/investment, edits there do nothing to positions here, and disabling that
extension does not stop this product.
The same is true of the Forex addon's investment plans and of Staking. If a customer's position is not on the screen you expect, the first question is which of the four products it belongs to.
The feature switch
Admin → System → Platform Settings → Features → Investment.
Nothing ever writes this row. initial.sql creates the settings table empty,
and no seeder adds an investment key. The purchase endpoint compares the
stored value against the exact string "true", and a missing key is not that —
so until the row exists, every purchase is refused with the 403 below and the
customer sees a product that will not sell.
The admin screen will not show you that. It renders each switch over a set of
built-in defaults, and investment is "true" among them, so the toggle reads
on over an empty database. Only fields you actually change are sent when you
save, so opening the page and pressing Save writes nothing either. To create the
row, toggle the switch off, then back on, and save. Then buy a plan once
yourself to confirm.
Two things read it, and only two:
- The purchase endpoint reads it on every buy. Off, and the request is
refused with
403 Investment feature is currently disabledbefore the plan is even looked up. This is a server-side refusal, not a hidden button. - The customer menu hides the Investments → Investment Plans entry that
points at
/investment.
Everything else stays where it is. The /investment pages still resolve if a
customer has the URL, the plans still list, the admin screens still work, and —
importantly — positions already running still settle. The settlement cron
does not consult this switch. Turning the feature off stops new money coming in;
it does not strand or accelerate the money already in.
That is the behaviour you want when withdrawing a product from sale. It is not what you want if you are trying to stop payouts, and there is no switch that does that.
The KYC gate
The purchase route calls assertKycFeature with one of two feature ids:
| Purchase | Feature id required |
|---|---|
type: "general" |
invest_general |
type: "forex" |
invest_forex |
Both go through the same endpoint. The forex branch creates rows the Forex
addon's own cron settles, so gating the whole endpoint on invest_general
would have let a verification level that you explicitly denied invest_forex
open forex positions through this door instead. That is why one route names two
features.
The gate is inert unless both master switches are on: KYC Verification
(kycStatus) and Enforce KYC Feature Access (kycFeatureEnforcement). The
second is off on every existing install by design — see
Settings reference — so on a stock platform nobody is checked
here at all. Turn it on only after reviewing every level in the level builder,
because a level that does not list invest_general loses the ability to invest
the moment you save.
The refusal a customer sees is "KYC verification is required to invest".
The settlement cron
processGeneralInvestments runs every hour, from the cron process, and is
registered in backend/src/cron/index.ts alongside the other core jobs. It is a
core job: it is loaded unconditionally in the scheduler's constructor, needs
no extension row, and appears on System → System Monitoring → Scheduled
Tasks as Process General Investments on every install.
Two consequences an operator meets in support tickets:
- A term ending is not a payout. A position that matures at 14:05 is paid on the next run, not at 14:05. Up to an hour of "why has my investment not completed" is normal and correct.
- A dead scheduler is invisible from the website. The site serves pages
perfectly while nothing settles. Investments pile up
ACTIVEpast theirendDate, and the only two places that say so are the Scheduled Tasks screen and the Overdue Settlements card on the investment history desk.
Viewing the job needs view.cron; triggering it by hand needs manage.cron.
The mechanics of what a run actually does — the ROI it pays, the outcome it
applies, what happens when one row fails — are in
The general investment lifecycle.
Permissions
Fifteen keys, five verbs across three objects. None of them is granted to any role by default.
| Object | Keys | Screen |
|---|---|---|
investment |
access · view · create · edit · delete |
Investment History (/admin/finance/investment/history) |
investment.plan |
access · view · create · edit · delete |
Investment Plans (/admin/finance/investment/plan) |
investment.duration |
access · view · create · edit · delete |
Investment Durations (/admin/finance/investment/duration) |
Grant access and view for a screen together or the table opens empty and
never fetches a row. That failure is silent — no error, no banner. See
Roles and permissions.
On the history desk those two keys do not create or amend a customer's position in any meaningful sense — they write columns with no money attached. Read The investment history desk before you grant either to anyone.
Where it appears in the admin
The menu group is Finance → Investment Management, with three entries:
| Entry | Path | Permission |
|---|---|---|
| Investment Plans | /admin/finance/investment/plan |
access.investment.plan |
| Investment Durations | /admin/finance/investment/duration |
access.investment.duration |
| Investment Analytics | /admin/finance/investment/history |
access.investment |
A role sees the group if it holds any of those three keys, and only the entries it holds.
There is no /admin/finance/investment index page. Typing that URL is a
dead end; the product has three leaf screens and no landing screen of its own.
The shortcut from the customer-facing investment pages therefore points at
/admin/finance/investment/plan.
Note the third entry is labelled Investment Analytics in the navigation but is the investment history table with an analytics header on it. Same screen, two names.
What else fires when an investment moves
Three side systems hang off this product. Each fails independently — a failure in any of them is logged and never blocks or reverses the money.
Affiliate rewards. Two conditions in
backend/src/utils/affiliate.ts belong to this product: INVESTMENT, paid to
the buyer's referrer at purchase time, and GENERAL_INVESTMENT, paid at
settlement. Both are event-driven — they are triggered inline by the route
and the cron, and the affiliate addon's periodic evaluator deliberately skips
them, because paying from both paths meant every referrer was paid twice under
two different idempotency keys. If you configure a reward for one of these
conditions, expect it once per event, not once per period.
Email. Purchases send NewInvestmentCreated; cancellations send
InvestmentCanceled; settlement sends InvestmentCompleted. Templates live on
System → Communication Tools → Notification Templates.
The seeder creates NewInvestmentCreated, InvestmentUpdated and
InvestmentCanceled, but there is no InvestmentCompleted row. On a stock
install the settlement email therefore fails to render inside the email worker
and is logged rather than sent. Notification templates can be edited and
enabled from the admin but not created — there is no create endpoint behind
that screen — so this is not something you can fix from the panel.
The payout, the in-app notification and the audit record are all unaffected; the customer simply does not get the "your investment has completed" email. If you need that mail, insert the template row directly or tell customers the in-app notification is the notice.
In-app notification. Settlement creates one linking to
/investment/<id>, with a View Investment action. It is typed system —
stored as SYSTEM, normal priority. The platform does have an INVESTMENT
notification type carrying high priority, but it is used by the AI
Investments, Forex and ICO products, not by this one, so these notices sort with
ordinary system messages rather than above them.
Turning the product off completely
There is no single kill switch, and the three levers do different things:
-
Stop new purchases — set Features → Investment off. Existing positions keep running and keep settling.
-
Stop the product being findable — the same switch hides the customer menu entry. The route itself still resolves for anyone with the URL.
-
Withdraw a particular plan from sale — set that plan's
statusto inactive on Investment Plans. It disappears from the plan list customers browse. Positions already open on it are unaffected and still settle.
The plan list endpoint filters on status: true, but the single-plan
endpoint and the purchase endpoint do not check it. Anyone holding the plan's
id — a bookmarked URL, a link in an old email, a script — can still open it and
still buy it after you switch it off. If a plan must genuinely stop taking
money, delete it rather than deactivating it, and read the deletion warning in
Investment plans and durations first.
Nothing here retires a running position. To wind one down early, the customer cancels it (principal refunded, no ROI) — or you let the term run out. Do not reach for the status actions on the history desk to do it for them; they do not move money.