The per-pool console
The sixth staking admin screen — the Details, Positions and Analytics tabs behind one pool, what its expected-versus-actual APR is really measured from, and the external performance log.
The admin screens lists five screens. There is a sixth, and it
is not in the navigation: the per-pool console at
/admin/staking/pool/{id}, reached by opening the View action on any row of
the Pools table.
It is where per-pool analytics live, where the external performance log is recorded, and where the Activate / Deactivate button for a single pool sits. It is worth knowing well, because two of its headline figures are computed differently from the same-named figures on the Overview screen.
frontend/middlewares/permissions.json lists /admin/staking/pool as
access.staking.pool and /admin/staking/pool/new as create.staking.pool.
Neither /admin/staking/pool/{id} nor /admin/staking/pool/{id}/edit is
listed, and the matcher fails through to the base access.admin for any admin
path it does not recognise. Any role holding access.admin can therefore open
this console and the pool edit form.
What it can see and do there is still gated server-side, action by action:
the Details and Analytics tabs read view.staking.pool, the Positions tab
reads view.staking.position, the status toggle needs edit.staking.pool, the
hero's Delete button needs delete.staking.pool, and the external
performance card needs view.staking.performance to list rows and
create.staking.performance to record one. A role holding none of them gets an
empty console, not a working one. Grade those six permissions individually —
the screen itself is not a boundary.
The header strip
The hero shows the pool's name, symbol, APR, lock period and its count of
ACTIVE positions, with four actions:
| Action | What it calls | Notes |
|---|---|---|
| Refresh | GET /api/admin/staking/pool/{id} |
Re-reads the pool with its positions, admin earnings, external performance rows and duration tiers |
| Activate / Deactivate | PUT /api/admin/staking/pool/{id} with {status} |
Toggles between ACTIVE and INACTIVE only. COMING_SOON is not reachable from this button — set it on the pool form |
| Edit | Navigates to /admin/staking/pool/{id}/edit |
The full pool form, including the duration tiers |
| Delete | DELETE /api/admin/staking/pool/{id} |
Refused while the pool holds ACTIVE or PENDING_WITHDRAWAL positions |
The status toggle enforces the one-active-pool rule — the status endpoint does not
The button goes through the full pool update endpoint, and that endpoint
re-checks the rule before saving: activating this pool while another ACTIVE
pool exists for the same symbol and walletType is refused with
An ACTIVE staking pool already exists for USDT (SPOT). Only one ACTIVE pool is allowed per symbol and wallet type.
There is a second, dedicated per-pool status route. It validates only that the
value is one of ACTIVE, INACTIVE, COMING_SOON and then writes it through
the generic status helper — it does not run the one-active-pool guard. The
same is true of the bulk PUT /api/admin/staking/pool/status.
Neither is wired to a screen: this console's toggle and the Pools table's edit
form both submit the whole record through the guarded update route. But if you
script a status change against a /status path, you can put two ACTIVE pools
on the same symbol and wallet type — the state the create and update routes
exist to prevent, and one the stake endpoint has no defence against. Send
PUT /api/admin/staking/pool/{id} with {"status": "ACTIVE"} instead.
Deactivating a pool stops new stakes and nothing else. Existing positions keep accruing and still settle on schedule — see Creating pools.
Details tab
A read-only panel of the pool's own columns: APR, admin fee, earning frequency, lock period, early-withdrawal fee, auto-compound, minimum and maximum stake, capacity, description, timeline, the risk boilerplate and the pool terms list.
A pool that publishes duration tiers mirrors its featured tier's rate, lock period, frequency and auto-compound flag onto the pool row. This tab reads those mirrored columns and nothing else, so a pool offering 30 / 90 / 365-day terms shows exactly one of them here, with no indication that the other two exist.
The tier list lives on the pool form (/admin/staking/pool/{id}/edit, the
Duration Tiers tab). Do not read this tab as the pool's full offer.
The capacity meter counts settled positions
The Pool Capacity card divides totalStaked by
totalStaked + availableToStake. totalStaked here is a raw
SUM(amount) over every position row in the pool — no status filter and no
soft-delete filter. Completed and cancelled positions are in it.
That is not the same number as anywhere else:
| Surface | Filled part of the bar |
|---|---|
This console, and the Pools table's totalStaked |
Every position ever opened in the pool |
| The Overview screen's pool pressure rows | ACTIVE + PENDING_WITHDRAWAL principal only |
GET /api/admin/staking/pool/{id}/analytics/performance |
ACTIVE + PENDING_WITHDRAWAL principal only |
The bar cannot read over 100%: both halves of the sum are non-negative —
staking is refused above availableToStake, and a negative capacityDelta is
refused unless there is that much headroom to take — so the ratio is bounded at
one. What it does instead is drift toward 100% as a pool churns, whatever room
is genuinely left, because a settled position stays in the numerator forever
while its principal is handed back to the headroom in the denominator. On a
pool that has been running long enough for positions to mature, the reading
stops meaning anything.
Use the Overview screen to judge whether a pool is out of room. The
availableToStake figure printed beside it is live and correct; it is the
percentage and the "total staked" figure that are cumulative.
Positions tab
The same position table as /admin/staking/position, scoped to this pool: same
columns, same analytics strip, same view dialog, and the same
Approve withdrawal / Reject withdrawal row actions on
PENDING_WITHDRAWAL rows, gated on edit.staking.position. Create and inline
edit are switched off here; delete and view are not.
The view dialog adds what the shared details panel does not carry — the full position UUID, when it was opened and closed, unclaimed rewards, the date of the last earning row, and the pool APR.
The pool-scoped list endpoint
This route differs from the global /api/admin/staking/position in exactly one
way that matters: the pool scope is forced from the path, and any poolId
the caller puts in the query filters is stripped before the query runs. A client
cannot widen it to another pool or to the whole book.
The tab on screen reads the global endpoint with a seeded poolId filter
instead, so the two produce the same rows for an operator; the forced-scope
route is what you want when you are handing an API key to something that must
only ever see one pool.
Both include the position's duration tier (id, name, lockPeriod, apr,
earningFrequency) on every row, read with soft-deleted tiers included so a
retired term still resolves rather than coming back null.
Analytics tab
Driven by GET /api/admin/staking/pool/{id}/analytics, with a range selector
(7d · 30d · 90d · 1y) and four sub-tabs — Overview, Earnings, Users and
Performance. The Export Data button is present on this tab.
The selector reads Last 7 days, Last 30 days, Last 90 days, Last year.
The endpoint computes, respectively: the current week (Monday-based), the
current calendar month, the start of the month two months back to the end
of this one, and the current calendar year. Everything is UTC, and daily
buckets are keyed off MySQL's DATE() on UTC-stored timestamps.
The response says which it measured, in range.periodType
(current_week · current_month · last_3_months · current_year) — but no
part of the screen prints it. On the 2nd of the month, "Last 30 days" is two
days of data.
Expected versus actual APR is not the Overview's promised-versus-realised
This is the single most important thing on the page to get right. The Pool Performance card and the Performance metric tile compare two numbers:
| Figure | Where it comes from |
|---|---|
| Expected APR | The pool's own apr column — i.e. the featured tier's rate |
| Actual APR | The mean apr of the last 10 staking_external_pool_performances rows for this pool, ordered by date descending |
| Efficiency | Actual ÷ Expected, badged as Exceeding (≥ 100%), Meeting (≥ 95%) or Below expectations |
Those external rows are the ones you type in by hand on this same tab (see below). Nothing about credited rewards, accruals or the wallet ledger enters this calculation.
That is the arithmetic doing exactly what it was told — the average of no rows is treated as zero — and not a solvency signal. Every internal pool, which is most of them, will show a red Below expectations badge here forever.
The figure that actually answers "am I paying what I advertise" is realised APR on the Overview screen, which is computed from rewards actually credited, annualised over principal-days and compared against the net promised rate. Read that one for solvency. Read this one only for external pools, and only as "what did the venue report versus what did I advertise".
The APR Performance chart on the Performance sub-tab has the same basis: one point per day in the window, carrying the external row's APR for that day or 0 where none was recorded, against a flat line at the pool's APR.
The rest of the tab
- Overview — staked amount, earnings and distinct stakers per bucket
(daily for
7d/30d, weekly for90d, monthly for1y), plus an earnings distribution donut counting earning rows into 0–10 / 10–50 / 50–100 / 100–500 / 500+ amount bands. - Earnings — earnings over time, and earnings split by row
type. - Users — activity, retention by position count (1 / 2 / 3+ positions), and
two derived boxes. "Retention rate" is
COMPLETEDpositions ÷ all positions, and "early withdrawal rate" isCANCELLEDpositions ÷ all positions. Cancellation is the punitive terminal state described in Positions, not an early exit — an approved early withdrawal ends asCOMPLETEDand lands in the retention figure. Treat that second box as a cancellation rate.
The endpoint no screen calls
Accepts timeframe of 24h, 7d, 30d, 90d or all. No admin screen
renders it today; it is there for reporting and integrations, and it is the
better source of the two because it is measured from real rows rather than
typed in:
metrics.totalValueLockedandmetrics.utilizationRateuseACTIVE+PENDING_WITHDRAWALprincipal — the correct basis, unlike the Details tab.historicalData.tvlHistory,positionHistoryandrewardsHistoryare reconstructed by walking an opening balance through each day's real inflows and outflows, capped at 366 buckets.CANCELLEDpositions carry no exit timestamp, so they cannot be placed on a timeline and are excluded from those three series. The count that was left out is reported inhistoricalData.meta.excludedCancelledPositions, alongsidefrom,to,granularityandtruncated.financialMetrics.effectiveAPYis a modelled figure — the pool's APR compounded 365 times a year — not a measurement of anything.
The external performance card
On the Performance sub-tab, and only when the pool has an
externalPoolUrl set. Internal pools do not get this form, deliberately: the
platform computes their performance itself, and a manual entry form would let an
admin record numbers that contradict the ledger.
It lists what has been logged for this pool — date, APR, total staked, profit, notes — newest first, and a Record button opens a dialog with those five fields.
poolId, date, apr, totalStaked and profit are all required; notes is
optional and an empty string is dropped rather than saved. apr and
totalStaked must be ≥ 0; profit may be negative. A date in the future is
rejected by the model with "date: Cannot be in the future", which the route
returns as a 400 validation error.
They are reference data. They do not fund a payout, do not change a position's APR, and do not appear in any user-facing surface. Their only consumers are the efficiency figures on this tab.
They exist so you can put what your real yield source produced next to what you promised stakers — the comparison this product cannot make for you, because the platform funds every reward out of your own float. See the warning on the product overview.
Recording a row also writes an admin notification, and its link is dead: it
points at a staking/performance path that no page.tsx serves. Clicking it
404s. The rows are only visible on this card.
Display order is not edited here
Two endpoints set the sequence pools appear in:
The POST form takes {"poolIds": [...]}, de-duplicates while preserving your
sequence, refuses unknown IDs, and then globally re-numbers all non-deleted
pools from 1 — the IDs you sent lead, everything else follows in its current
order. The PUT form takes {"poolOrders": [{poolId, order}, ...]} and refuses
duplicate IDs or duplicate order values, writing exactly what you send and
touching nothing else. It also writes one activity-log row per pool.
A store action for the POST form exists in the frontend but nothing calls it.
In practice you set a pool's position with the Display order field on the
pool form's Basic Info tab, or through the Pools table's inline edit form.
Either way it is presentation only. Order decides the sequence of the public pool list and nothing economic — not capacity, not priority for accrual, not which pool a stake lands in.
What is not on this console
- Duration tiers. Added, edited, retired and featured on the pool form
(
/admin/staking/pool/newand/admin/staking/pool/{id}/edit, the Duration Tiers tab). See Duration tiers. - Capacity changes. The
availableToStakeinput on the edit form is inert; capacity moves only through an explicitcapacityDeltaonPUT /api/admin/staking/pool/{id}. See Creating pools. - Reward distribution. The Earnings screen carries one of the two doors —
its Distribute dialog posts to
/api/admin/staking/earning/distribute(singular), the one-offBONUSpayout. The APR accrual catch-up,POST /api/admin/staking/earnings/distribute(plural), is on no screen at all: it is an API-only route, like/pool/{id}/analytics/performanceabove. Call it with an API client. See Rewards. - The activity log. Written by this console's actions, read at
GET /api/admin/staking/activity.