Creating plans

Every field on the AI investment plan form — which two decide the payout, which are shown to users, which are decorative, and why a plan can never be deleted once anyone has invested in it.

6 min readUpdated 3 August 2026plans, profit, configuration, admin

A plan is the product your users buy. It carries the entry limits, the marketing copy, the list of terms it can be bought on, and — the part that matters — the outcome and the rate every investment against it will settle at.

Plans live at /admin/ai/investment/plan. You need access.ai.investment.plan to open the screen and create.ai.investment.plan to add one.

Create a duration first

The plan form requires at least one duration, and the picker is populated from the durations that already exist. On a fresh install there are none, so the create form cannot be completed.

Go to /admin/ai/investment/duration and add at least one term before you start here. Durations covers what a term actually means.

The two fields that decide the payout

Everything else on this form is presentation or policy. These two are the deal.

Profit as a percentage of the principal. 5 means 5%. This is the rate settlement pays and the rate the purchase panel quotes.
The outcome every investment against this plan settles with. WIN pays principal plus profit, LOSS returns principal minus profit, DRAW returns the principal only. The column is NOT NULL with no database default — the admin form preselects WIN because it is the first option in the enum, but a plan created through the API must supply it.

Both are copied onto each investment at the moment it is created, not read at maturity. That snapshot is what makes the quote binding: editing the plan afterwards changes what new investments will pay and leaves every investment already running exactly as it was sold.

There is no mechanism that overrides it. It is not a starting value that some model later revises, and there is no per-investment override on any screen. Every investment placed against a plan set to LOSS will lose, and every investment placed against a plan set to WIN will win, in the exact same proportion.

The only thing that can change a specific investment's outcome after purchase is an administrator editing the row directly in the Investment Logs table — and that edit writes the outcome without moving any money.

A LOSS plan takes amount × profitPercentage / 100 off the principal returned, floored at zero. A plan configured to take more than 100% cannot make a user owe you money; the payout simply becomes nothing.

Entry limits

Smallest amount a single investment may be, in the quote currency of the market. Enforced server-side on every purchase.
Largest amount a single investment may be. Enforced server-side on every purchase.

The check is inclusive at both ends and rejects anything outside with a message naming both bounds. There is no per-user cap, no daily cap and no total-book cap anywhere in this product — maxAmount is the only ceiling you have, and nothing stops one user opening a hundred investments at that ceiling.

Both are compared against a plain number, so they are denominated in whatever the market's quote currency happens to be. A plan with minAmount 100 means 100 USDT on BTC/USDT and 100 USDC on ETH/USDC. If you run markets quoted in currencies of very different value, the same plan will be a very different product on each of them.

Fields users see but nothing calculates

Minimum profit percentage. Stored, returned to the browser, never used in any calculation.
Maximum profit percentage. Stored, returned to the browser, never used in any calculation.
Total amount currently invested in the plan. A number you type in. Nothing in the addon ever updates it.

All three are required by the create form, all three are sent to the browser with the plan list, and none of them affects a payout.

invested deserves particular care. It reads as a live figure — the purchase panel renders it on the plan card next to the profit rate, labelled Invested — but no code path increments it when someone invests or decrements it when someone cancels. It is a static marketing number you maintain by hand. If you want the real figure, the admin dashboard's plan distribution and the Investment Logs analytics both compute it from the investments themselves.

minProfit and maxProfit look like they define a range the return is drawn from. They do not. There is no randomisation anywhere in this addon.

Presentation

Internal plan name. Used in the admin tables, the dashboard's plan distribution chart and the top-plans list. Not returned by the public plan endpoint.
Public title shown to users. This is the name on the plan card and in every email.
Plan description, up to 500 characters on the form.
Plan image. Must be a path under /uploads or /img — an external URL is rejected by the model validator.
Marks the plan as trending. Sorts it to the top of the admin dashboard's top-plans list and drives the Trending filter on the user-facing plan list.
Whether the plan is offered. Only plans with status true are returned to users.

name and title are separate on purpose and it is easy to get them the wrong way round: title is what your customers read, name is what you read in the admin tables. The public plan endpoint deliberately does not include name.

The image validator only accepts a path beginning /uploads or /img. Pasting an image URL from another host fails the save with a validation error rather than producing a broken picture later.

What users never see

Two columns are excluded from the public plan endpoint by name:

  • defaultProfit
  • defaultResult

defaultResult is withheld for the obvious reason — serving it would let anyone with the network tab open read whether a plan wins or loses before placing a single investment. defaultProfit goes with it because it is the operator's configured figure rather than the advertised one.

defaultProfit is a required field on the form and is only ever used as a fallback: if profitPercentage is null, the snapshot taken at purchase falls back to defaultProfit. On any plan created through the admin form both are set, so defaultProfit never comes into play. Set it to the same value as profitPercentage and forget about it — a plan where the two disagree is a plan that will pay one number in some circumstances and a different number in others.

Attaching durations

The Duration options group on the form is a multi-select fed by /api/admin/ai/investment/duration/options, which returns every duration formatted as 30 DAY, 1 MONTH and so on. Selecting them writes rows into the ai_investment_plan_duration join table.

The link is enforced at purchase: a request naming a plan and a duration that are not joined is refused with "Duration not available for this plan". A plan with no durations attached cannot be invested in at all.

There is no per-duration pricing. A plan pays the same profitPercentage whether it is bought on a 1 HOUR term or a 12 MONTH one. If you want a longer term to pay more, that is a separate plan.

Activating and deactivating

The status column renders as a toggle in the plans table, so you flip it in place. Only plans with status true are returned by the public plan endpoint, so switching it off removes the plan from the purchase panel immediately.

Deactivating does not touch investments already running against the plan. They settle on their snapshotted terms exactly as if nothing had changed. That is what makes deactivation the correct way to retire a plan.

Deleting a plan

Deletion — single or bulk — is refused with a 409 while any investment references the plan, counting ACTIVE investments separately from completed, cancelled and rejected ones, and the message tells you how many of each.

The refusal covers historical investments too, not just live ones. A settled investment whose plan has been deleted is a record nobody can interpret, and before this guard existed a soft-deleted plan left every live investment against it stranded: settlement could not read the plan's terms, failed on every hourly pass, and the principal stayed locked forever with no way to recover it from any screen.

Deactivate instead. There is no situation in which deleting a plan is the right move.

If you have investments already orphaned by a deletion that predates the guard, settlement now rescues them: they clear as a DRAW on the next hourly run, which returns the principal in full and invents no profit from a plan that no longer exists. Lifecycle has the full precedence.

Editing a running plan

Every field is editable at any time. Because the payout terms are snapshotted at purchase, an edit is safe in the sense that it cannot reprice a deal already struck — but it does change what the next investment pays, and users who bought yesterday and today can hold materially different contracts against a plan with one name.

Two edits worth thinking twice about:

  • Raising minAmount above an existing investment's amount. Nothing revalidates old rows, so the investment continues normally. The limits are entry checks only.
  • Changing defaultResult. New investments flip to the new outcome from the moment you save. If your plan has been paying WIN and you switch it to LOSS, the user who buys thirty seconds later loses money on a product that has never lost before, with nothing on any screen marking the change.

Next: Durations — how a term is measured, and the one delete in this addon that used to destroy money.