Durations

How an AI investment term is measured — the four timeframes, why a month is always 30 days, how durations attach to plans, and the delete that used to destroy live investments.

4 min readUpdated 3 August 2026durations, timeframe, maturity, admin

A duration is the term an investment runs for. It is a deliberately tiny record — a number and a unit — shared across every plan that offers it. Durations live at /admin/ai/investment/duration behind access.ai.investment.duration.

There are only two fields.

How many units the term runs for. A whole number, minimum 1.
The unit the number is counted in.

timeframe accepts exactly four values: HOUR, DAY, WEEK, MONTH. Anything else is rejected by the model.

Everywhere a duration is shown — the plan form's picker, the admin tables, the purchase panel, the emails — it is rendered as the number and the unit with a space between them: 30 DAY, 1 MONTH, 6 HOUR. There is no pluralisation and no separate display label.

How maturity is calculated

Settlement compares the investment's createdAt against a computed end date. There is no separate endDate column on the row; it is derived every time.

timeframe End date
HOUR createdAt + duration hours
DAY createdAt + duration days
WEEK createdAt + (duration × 7) days
MONTH createdAt + (duration × 30) days

MONTH is not calendar arithmetic. It is multiplication by 30. A 1 MONTH investment opened on 31 January matures on 2 March, and a 12 MONTH investment runs for 360 days rather than a year.

If you are publishing an annualised return alongside a 12 MONTH term, your users are getting the stated percentage over 360 days, which is a slightly higher effective rate than the one you advertised. Use 365 DAY if that matters to you.

An unrecognised timeframe falls back to hours. That branch is unreachable through the admin form because the enum is validated at both the schema and the model level, but it is worth knowing that a value written directly into the database does not fail loudly — it silently produces a term measured in hours.

Precision

Maturity is only checked when settlement runs, so the term is a floor rather than an exact moment. An investment on a 1 HOUR duration matures sixty minutes after it was created and is settled on the next hourly cron pass after that, which can be anything up to an hour later — or immediately, if the owner opens their investment list in between.

For short terms that skew is proportionally large. A 1 HOUR investment can take close to two hours to pay. If you sell hourly products, tell your users the term is a minimum.

Attaching durations to plans

Durations exist independently of plans and are joined to them through the ai_investment_plan_duration table. You make the link from the plan form, not from here — the Duration options multi-select on the plan form writes the join rows.

The link is enforced on purchase. A request naming a plan and a duration that are not joined is refused with "Duration not available for this plan", so a user cannot construct a term you have not offered by editing the request.

One duration can be attached to any number of plans, and removing it from one plan does not affect the others.

Deleting a duration

ai_investment_duration is not soft-deleted, and ai_investment.durationId carries an ON DELETE CASCADE foreign key. Deleting a duration is therefore a real delete, and the cascade used to take every investment that referenced it — including ACTIVE ones whose principal had already been debited from the user's wallet.

The money was simply gone: no refund, no investment record, and nothing on any screen to reconcile against. One click, behind nothing more than the standard delete confirmation.

Deletion is now refused with a 409 while any investment references the duration, live or historical, single delete and bulk delete alike. The message says how many investments are involved.

There is no way to retire a duration cleanly, because a duration has no status column. What you do instead is remove it from every plan's Duration options. Existing investments keep their durationId and settle normally, and nobody can buy on that term again.

If you suspect a duration was deleted on an install predating the guard, the investments are unrecoverable but the funding debits survive. This query lists users who were charged for an investment that no longer exists:

SELECT t.userId, t.amount, t.currency, t.createdAt, t.referenceId
FROM transaction t
LEFT JOIN ai_investment i ON i.id = t.referenceId
WHERE t.type = 'AI_INVESTMENT' AND i.id IS NULL;

Each row is a refund decision you have to make by hand.

What durations do not do

Several things you might reasonably expect from a term product are absent, and knowing that up front saves a lot of searching:

A plan pays the same profitPercentage on every duration attached to it. A 1 HOUR investment and a 12 MONTH investment in the same plan return the identical percentage of principal.

If you want longer terms to pay more, create separate plans. There is no field anywhere that varies the rate by term.

Nothing rolls over. An investment settles once, credits the wallet, and ends. A user who wants to reinvest opens a new investment manually.

Nothing stops you creating 30 DAY twice. The table has no unique index across duration and timeframe, and the picker on the plan form will show both, identically labelled, with no way to tell them apart.

Two identical durations are not harmful — investments against either behave the same — but the plan form becomes confusing and the duration table starts to look like it has a bug. Check the list before you add.

The duration table has timestamps: false, so there is no createdAt to sort by and no record of when a term was added or last changed. The list is sorted by the duration number by default.

Next: Lifecycle — what happens between the debit and the payout, and the exact arithmetic settlement uses.