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.

4 min readUpdated 3 August 2026api, endpoints, permissions, database

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

Every active plan with its attached durations. Public — no session required.
The caller's investments, paginated. Settles any of their matured investments in-line first.
Open an investment. Debits the wallet and snapshots the payout terms.
One investment, owner only. Settles it in-line if it has matured.
Cancel an ACTIVE investment and refund the full principal.

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.defaultProfit and plan.defaultResult are deleted, always.
  • On an investment whose status is ACTIVE, result, roiPercentage and profit are 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

Overview counts, results, chart data, plan distribution, recent investments and top plans

Accepts ?timeframe=1m|3m|1y, defaulting to 1y. The timeframe affects the chart series only; every overview figure is install-wide.

Investments

List every investment, with filtering, sorting and pagination
Create an investment record. Writes no transaction and debits no wallet — see the warning below
Bulk status change. Settles or refunds each id through the shared authority
Bulk delete
One investment in full
Edit an investment. Refuses amount, userId, type and status
Delete or restore. Refunds only when the principal is still outstanding
Settle, cancel or reject one investment. Moves money

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

List plans, including their investments and attached durations
Create a plan and attach its durations
Bulk activate or deactivate
Bulk delete. Refused with 409 while any investment references a plan
One plan in full
Update a plan
Delete a plan. Refused with 409 while any investment references it
Activate or deactivate one plan

Create and update require name, title, invested, profitPercentage, minProfit, maxProfit, minAmount, maxAmount, defaultProfit and defaultResult. durations is an array of duration ids.

Durations

List durations, sorted by the duration number
Create a duration
Bulk delete. Refused with 409 while any investment references a duration
Durations pre-formatted as picker options, e.g. 30 DAY
One duration
Update a duration
Delete a duration. Refused with 409 while any investment references it

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.