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.
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.
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
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
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
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:
defaultProfitdefaultResult
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
minAmountabove 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 payingWINand you switch it toLOSS, 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.