API and data model
Every AI Investments endpoint — user and admin — with the permission each admin route gates on, the four tables and their columns, and the transaction and reference conventions settlement depends on.
Two API surfaces. Everything under /api/ai/investment is scoped to the calling
user and carries no permission — the purchase route is gated by a KYC feature
instead. Everything under /api/admin/ai/investment carries an explicit
permission.
Conventions that will catch you out
The platform pins the HTTP status at 200 and puts the real outcome in the body. A bulk status change that settled three of ten investments returns a body saying so; a client that branches on the status code will report a clean sweep.
amount, profit and roiPercentage are DOUBLE columns rather than
DECIMAL, so they arrive as JSON numbers rather than strings — the opposite of
most money columns on this platform. They are still money, and double-precision
arithmetic is still inexact; do not compare two computed payouts for equality.
symbol is a free-text BASE/QUOTE string built at purchase from the market the
user was viewing. Settlement splits on / and takes the second half as the
currency. A symbol without a slash breaks that fallback.
User endpoints
GET /api/ai/investment/plan
Returns only plans with status true. The selected columns are id, title,
description, image, minAmount, maxAmount, profitPercentage,
minProfit, maxProfit, invested, trending, status, plus each attached
duration's id, duration and timeframe.
name, defaultProfit and defaultResult are deliberately excluded. Serving
the last two would let anyone reading the response know whether a plan wins or
loses before placing an investment.
This route declares no requiresAuth, so it answers unauthenticated callers.
Treat everything it returns as public.
GET /api/ai/investment/log
limit defaults to 20 and is clamped to 100; offset defaults to 0. Returns
{ items, pagination: { total, limit, offset } }, ordered by status then
creation date.
Before it reads the page, it settles every matured ACTIVE investment belonging
to the caller — across all pages, not just the one being returned. Errors there
are swallowed so a failing settlement never blocks the list.
POST /api/ai/investment/log
{
"planId": "…",
"durationId": "…",
"amount": 1000,
"currency": "BTC",
"pair": "USDT",
"type": "SPOT"
}All six are required. currency and pair are the base and quote of the market
being viewed; the wallet debited is the one holding pair, and the stored
symbol is currency/pair. type is SPOT or ECO.
Refusals, in the order they are checked: not signed in (401), KYC feature
invest_ai not held, plan not found (404), plan not active (400), duration not
found (404), duration not attached to the plan (400), amount outside the plan's
limits (400), wallet not found (404), insufficient funds (400).
The response is a bare { "message": "Investment created successfully" } — the
investment object is not returned. Re-read the list to get it.
DELETE /api/ai/investment/log/{id}
Owner only, ACTIVE only. Refunds the full principal, marks the row CANCELLED,
then soft-deletes it. The original funding transaction is preserved and annotated
rather than destroyed.
KYC gate
| Feature | Guards |
|---|---|
invest_ai |
POST /api/ai/investment/log only |
Browsing plans, reading investments and cancelling carry no gate.
What is withheld from users
Every user-facing response passes through one serialiser:
plan.defaultProfitandplan.defaultResultare deleted, always.- On an investment whose status is
ACTIVE,result,roiPercentageandprofitare set to null rather than removed, so the shape stays stable and a null reads as "not decided yet".
Both values reappear as normal once the investment settles.
Admin endpoints
Dashboard
Accepts ?timeframe=1m|3m|1y, defaulting to 1y. The timeframe affects the
chart series only; every overview figure is install-wide.
Investments
Both status doors accept COMPLETED, CANCELLED or REJECTED and require the
investment to be ACTIVE. The bulk door returns { message, settled, failed }
and throws a 400 when nothing at all could be moved, rather than reporting a
success for a batch of payouts that never happened.
The edit door writes only planId, durationId, symbol, profit and
result. The admin table narrows that further, offering just profit and
result, and only on ACTIVE rows.
It is a plain record write. No wallet is debited, no funding transaction is created and no payout terms are snapshotted. Settlement will still pick the row up, fall back to resolving a wallet by user, currency and type, and credit principal plus profit that was never taken from anybody.
The admin table does not expose a create button for exactly this reason.
Plans
Create and update require name, title, invested, profitPercentage,
minProfit, maxProfit, minAmount, maxAmount, defaultProfit and
defaultResult. durations is an array of duration ids.
Durations
Durations have no status column, so there is no status endpoint.
Tables
If you read the database directly.
ai_investment
Soft-deleted (paranoid), so filter on deletedAt IS NULL unless you want
cancelled rows.
| Column | Type | Notes |
|---|---|---|
id |
UUID | Primary key |
userId |
UUID | Owner |
planId |
UUID | ON DELETE CASCADE to ai_investment_plan |
durationId |
UUID, nullable | ON DELETE CASCADE to ai_investment_duration |
symbol |
VARCHAR(191) | BASE/QUOTE; settlement reads the quote half |
type |
ENUM | SPOT, ECO — which wallet was debited and will be paid |
amount |
DOUBLE | The principal, in the quote currency |
profit |
DOUBLE, nullable | Absolute ROI amount. Deprecated as an input, still written on every settlement |
roiPercentage |
DOUBLE, nullable | Canonical: profit as a percentage of the principal. Snapshotted at purchase |
result |
ENUM, nullable | WIN, LOSS, DRAW. Snapshotted at purchase |
status |
ENUM | ACTIVE, COMPLETED, CANCELLED, REJECTED. Default ACTIVE |
createdAt |
DATETIME | Maturity is measured from here. There is no endDate column |
ai_investment_plan
Soft-deleted.
| Column | Type | Notes |
|---|---|---|
id |
UUID | Primary key |
name |
VARCHAR(191) | Internal. Not served to users |
title |
VARCHAR(191) | Public |
description |
TEXT | Public |
image |
VARCHAR(1000) | Must match ^/(uploads|img)/.*$ |
status |
BOOLEAN | Default true. Only true plans are served to users |
invested |
DOUBLE | Static; nothing updates it |
profitPercentage |
DOUBLE | The rate settlement pays |
minProfit, maxProfit |
DOUBLE | Stored and served; never used in a calculation |
minAmount, maxAmount |
DOUBLE | Entry limits, checked inclusively |
trending |
BOOLEAN | Sorting and filtering only |
defaultProfit |
DOUBLE | Fallback rate when profitPercentage is null. Admin-only |
defaultResult |
ENUM | WIN, LOSS, DRAW. Admin-only |
ai_investment_duration
Not soft-deleted, and carries no timestamps. id, duration (INTEGER,
minimum 1) and timeframe (ENUM HOUR, DAY, WEEK, MONTH). No unique index
across the pair, so duplicates are possible.
ai_investment_plan_duration
The join table. id, planId, durationId. No timestamps. A row here is what
makes a duration selectable for a plan and what the purchase check validates
against.
Transactions and references
transaction.referenceId carries a platform-wide unique index, which is why
every leg of one investment's life uses a different suffix.
| Event | transaction.type |
referenceId |
Idempotency key |
|---|---|---|---|
| Funding debit | AI_INVESTMENT |
<id> |
investment_<id> |
| Maturity payout | AI_INVESTMENT_ROI |
<id>_roi |
ai_invest_cron_payout_<id>_<RESULT> |
| Admin completion payout | AI_INVESTMENT_ROI |
<id>_roi |
ai_invest_admin_payout_<id> |
| User cancellation refund | REFUND |
<id>_refund |
investment_refund_<id> |
| Admin cancel or reject refund | REFUND |
<id>_refund |
ai_invest_admin_<status>_<id> |
| Admin delete refund | REFUND |
<id>_admin_refund |
admin_ai_investment_delete_refund_<id> |
| Admin bulk-delete refund | REFUND |
<id>_admin_refund |
admin_ai_investment_bulk_delete_refund_<id> |
Both payout doors write the same <id>_roi reference under different idempotency
keys, so a second payout collides on the unique index even when the keys differ.
Settlement reads that collision as proof the ROI was already paid: it commits the
terminal status without re-crediting, and suppresses the completion email.
The platform's own leg is written to adminProfit under type AI_INVESTMENT,
with reference <id>_payout for a WIN (recorded as a platform loss) or
<id>_house for a LOSS (credited to the Super Admin's wallet as revenue). A
DRAW writes nothing.
Scheduled task
| Name | Category | Period | Does |
|---|---|---|---|
processAiInvestments |
ai_investment |
1 hour | Scans every ACTIVE investment, settles the matured ones |
Titled Process AI Investments at /admin/system/cron. Registered only while
the ai_investment extension row is enabled, and re-evaluated periodically so
toggling the extension does not need a restart. The same routine also runs
in-line on the two user-facing read endpoints.
There is no WebSocket surface for this addon.