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.

7 min readUpdated 6 August 2026investment, kyc, cron, permissions, features

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

Master switch for the general investment product. Stored as text — the string "true" or "false", not a boolean. There is no database default and nothing seeds the row; an absent row reads as off.

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 disabled before 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 ACTIVE past their endDate, 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:

  1. Stop new purchases — set Features → Investment off. Existing positions keep running and keep settling.

  2. Stop the product being findable — the same switch hides the customer menu entry. The route itself still resolves for anyone with the URL.

  3. Withdraw a particular plan from sale — set that plan's status to 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.