Plans and durations

Building the investment products your customers buy — every field on a forex plan, which two of them decide what you actually pay out, how durations attach, and the validation that stops a dead plan reaching the site.

6 min readUpdated 3 August 2026plans, durations, profit, currency

A plan is the product. A duration is the term. A customer buys the pair, and the pair decides what leaves your balance sheet at maturity. Build durations first — a plan cannot be saved without at least one.

Durations

Admin → Forex → Plans → Durations, at /admin/forex/duration.

A duration is two fields: a whole number and a timeframe of HOUR, DAY, WEEK or MONTH. That is all it holds — there is no rate, no label and no description on the row itself.

The end date is computed when the investment is created, from the moment of creation:

Timeframe Adds
HOUR that many hours
DAY that many days
WEEK that many × 7 days
MONTH that many × 30 days

Not a calendar month. A three-month investment opened on 31 January ends on 31 March, not 30 April. If you advertise calendar months anywhere in your marketing, your customers will eventually notice the gap.

Durations are shared across every plan on the platform. Attaching them is what scopes them — see below.

forex_investment has a database-level cascade to forex_duration. Deleting a duration used to physically remove every investment on it, principal and history together, with nothing to settle and nothing to refund.

Deletion is now refused while active investments reference the duration, and a permanent delete is refused while any investment does — settled ones included, because those rows are what customer statements and your accounting are built from. Settle or cancel first; cancelling refunds the principal.

Plans

Admin → Forex → Plans, at /admin/forex/plan. The create form has six groups.

Plan identity

name is required and is what the tables and settlement logs use. title, description and image are the customer-facing presentation and are all optional — but a plan with no image draws a placeholder on the plan cards, so supply one.

Wallet and currency

Both required, and together they are the most consequential choice on the form. The wallet type list is built from what your install actually supports, and the currency list refreshes from it.

The wallet type dropdown offers ECO as soon as the Ecosystem addon is installed, because it is the platform's generic wallet-type list. This addon cannot use it: the fee calculation behind every forex deposit and withdrawal handles FIAT and SPOT only and refuses anything else with Invalid wallet type.

A plan denominated in an ECO currency saves, publishes and appears on the site. Every attempt to fund an account for it then fails with a 400, and the customer has no way to tell why. Use FIAT or SPOT.

The plan's currency and wallet type must also match the customer's forex account. An account is bound to one currency and one wallet type the first time it is funded, and an investment against a plan in a different denomination is refused with a message naming both. So a customer holding a SPOT USDT forex account can only invest in SPOT USDT plans. If you want to offer more than one denomination, understand that a customer must keep a separate account per currency and that the product only issues one LIVE account per person — in practice, run one denomination.

Investment limits

minAmount and maxAmount bound what a single investment may be. Both are optional; a null maximum means unlimited.

They are enforced twice — once by the investment route and once by the fraud checks, which use the plan's own maxAmount as the ceiling rather than a hidden literal. A negative or non-numeric amount is rejected outright, regardless of what the limits say.

Profit configuration

Five fields, and this is where operators go wrong.

The percentage of the principal that settlement actually pays. This is the number
The bottom of the range you advertise. Used for validation and for the customer-facing plan filter
The top of the range you advertise. Used for validation
A fallback percentage, used only when a plan has no profit percentage set
The outcome every investment on this plan gets unless an admin sets one on the investment itself. 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.

Settlement computes the movement as principal × profitPercentage ÷ 100 and then applies the result:

  • WIN — the forex account gets principal + profit
  • LOSS — the forex account gets principal − profit, never below zero
  • DRAW — the forex account gets the principal back

minProfit and maxProfit do not enter that calculation at all. They bound what you may set profitPercentage to, and they are what the plan card shows as a range. A plan advertising "5–20%" with a profit percentage of 12 pays exactly 12% to every investor on it.

This is the point to be honest with yourself about the product. There is no market behind an investment. A plan with default result WIN and profit percentage 15 is a promise to hand every investor 15% of their principal at maturity, out of your own funds, on a schedule you cannot pause once the investments are open. Model that before you publish.

Per-investment overrides are possible: an admin can set result on an individual investment at /admin/forex/investment before it settles, and that value wins over the plan's default. It is the only way to make one investment differ from another on the same plan.

Duration options

Required. Pick every duration this plan should offer; the picker is fed from your durations list.

The investment route checks the forex_plan_duration join before it does anything else and refuses with That duration is not offered by this plan. This is deliberate — because a plan's profit is a flat percentage that does not scale with time, an unscoped duration list would let a customer take a 30-day plan's whole return in one hour.

The consequence is that a plan whose durations were never attached is completely dead: it lists, it opens, and every investment on it is refused.

Plan settings

status and trending. Both default to off, and status defaulting to off is the one that catches people — a plan you have just created is invisible until you enable it, because the customer-facing plan list only returns plans with status true. A disabled plan also refuses new investments outright, so switching an active plan off is a safe way to stop taking money on it.

trending promotes the plan into the Trending tab on /forex/plan and onto the landing page's featured row. It changes nothing else.

What the validation refuses

Both the create and the update routes run the same checks before writing anything. Each returns a 400 with the reason:

Refused when Why it matters
Any of the six numeric fields is negative A negative percentage inverts the payout
maxAmount is below minAmount No amount could ever satisfy both — the plan is dead on arrival
maxProfit is below minProfit The advertised range is inverted
profitPercentage sits outside minProfitmaxProfit Investors are paid the percentage, so the two must agree

The third and fourth are the ones that used to ship broken plans. Transposing the limits — minimum 5,000, maximum 100 — saved happily and put a plan on the site where every possible amount was refused with Amount must be between 5000 and 100, whatever the customer typed.

Changing a live plan

Editing a plan changes the terms of investments that have not settled yet, because settlement reads the plan at maturity rather than a snapshot taken at purchase.

An investment holds its own principal, duration and end date, but not its rate. Change profitPercentage from 10 to 25 and every open investment on the plan settles at 25% — including ones bought weeks ago on the old terms.

If you need to change what a plan pays going forward, create a new plan and disable the old one. The disabled plan stops taking money immediately and its open investments continue to settle on the terms they were sold under.

Changing the currency or wallet type of a plan that already has open investments is worse: those investments were funded from accounts bound to the old denomination, and the mismatch check will refuse anything new while the old ones settle into an account whose currency no longer matches the plan. Do not do it. Make a new plan.

What the customer sees

GET /api/forex/plan returns only enabled plans, and deliberately excludes defaultProfit, defaultResult and status from the payload — so the outcome you have configured is never visible to the person buying. It also computes an invested total per plan, which is what the default popularity sort orders by.

Customers can filter by trending, search title and description, set a minimum profit, cap the maximum investment, and sort by popularity, profit or minimum investment.

Next: Accounts — where the money the plan consumes has to be sitting first.