The admin panel
A tour of the fifteen NFT admin screens — the moderation dashboard and its five queues, the content and trading tables, creator verification, the dispute board, and the onboarding checklist that is not in the navigation.
The NFT admin area lives at /admin/nft and has fifteen screens grouped into
four navigation sections. Thirteen are in the menu; two — Disputes and
Onboarding — are reachable only by URL or by a link from another screen.
| Section | Screens |
|---|---|
| — | Dashboard (/admin/nft) |
| Content | Categories, Collections, NFTs |
| Trading | Marketplace, Listings, Offers, Auctions, Sales |
| Community | Creators, Activity |
| System | Analytics, Settings |
| Not in the menu | Disputes (/admin/nft/dispute), Onboarding (/admin/nft/onboarding) |
Every screen carries its own permission. Granting access.nft alone opens
almost nothing — see Permissions.
The moderation dashboard
/admin/nft answers exactly one question: what needs a decision right now?
It deliberately does not show collection counts, sales volume or average price. Those are scoreboards; none of them changes what you do in the next ten minutes. Instead it publishes five queues, each backed by a real row state somebody wrote deliberately, each with a count, the age of the oldest item waiting, and — where money is frozen — the amount.
nft_collection.status = 'PENDING'. Nobody can mint into these. The door is the
status toggle on the Collections screen.
The four non-terminal nft_dispute states, with the HIGH/CRITICAL and
unassigned subsets called out separately. The dispute SLA is 24 hours — the
tightest budget on the platform, tighter than a withdrawal, because escrow is
held while a dispute runs and both sides are frozen out of their own money.
nft_listing.settlementBlockedAt is stamped when an auction ends with a winning
bid and there is no on-chain auction contract holding the money. A winner
exists, bids were placed, and nothing will move until a human acts. Deploying an
auction contract for the listing clears the flag.
nft_offer.flaggedAt on an ACCEPTED offer, written by the stale-offer sweep
when a buyer's escrow could not be released. Their funds are locked and they
hold no NFT. This is the queue to work first.
Three token states that cannot support what the rest of the product asks of them:
- a listed token with no metadata URI or no image renders as a blank card with nothing to show a buyer;
- a token marked minted with no
blockchainTokenIdis rejected outright by transfer, approval and listing — it looks minted and can never move; - a DRAFT older than 72 hours is a batch preparation that was never carried out.
The headline is a sum of defects, not of distinct tokens: one row can carry more than one, and the interface says so.
Every counter is a SQL aggregate, not a page summed in the browser. Only the two preview lists are capped, and the response declares that cap.
A timeRange of 24h, 7d or 30d controls the dispute inflow-versus-
resolution series.
Content screens
Categories
/admin/nft/category — the only NFT table where an admin can create, edit and
delete rows. Collection creation requires a valid category id, so with zero
categories nobody can create anything. Create at least two.
Collections
/admin/nft/collection — read-mostly. Create and delete are switched off in the
interface because collections belong to their creators. What an admin does here
is approve (the status toggle) and correct metadata.
The toggle takes a boolean: true → ACTIVE, false → SUSPENDED. There is no
route back to PENDING.
NFTs
/admin/nft/token — every token in the marketplace. Read-only apart from a
status toggle, which maps true → MINTED and false → BURNED.
There is no SUSPENDED state on nft_token. Switching a token off writes
status: 'BURNED' — the same terminal state a real burn produces. Use it for
genuinely unwanted content, not as a temporary hide.
Trading screens
Marketplace
/admin/nft/marketplace — contract deployment, configuration, emergency
controls and fee withdrawal. This is the only screen that spends money, and it
has its own page: Marketplace contracts.
Listings, Offers, Auctions, Sales
Four tables over nft_listing, nft_offer and nft_sale. All four are
read-only in the interface — create, edit and delete are all switched off — with
a single status action each:
| Screen | Table | Status action |
|---|---|---|
| Listings | nft_listing |
Toggle a listing on or off |
| Offers | nft_offer |
Toggle an offer on or off |
| Auctions | nft_listing filtered to type = AUCTION |
Toggle an auction on or off |
| Sales | nft_sale |
None — sales are a record, not a workflow |
The Auctions screen is the Listings model with a fixed filter, so a row appearing on both is the same row.
There is a second, richer listing-moderation endpoint that the tables do not
call — it accepts a status of ACTIVE, CANCELLED or EXPIRED plus a
moderation note and a flag reason:
Community screens
Creators
/admin/nft/creator — creator profiles, created automatically the first time a
user makes a collection or mints. Two controls:
Verification tiers are BRONZE, SILVER, GOLD and PLATINUM. Setting a tier
sets isVerified to true automatically — you cannot have a tiered but
unverified creator.
The onboarding checklist looks for at least one verified creator, so verifying somebody is part of a clean launch.
Activity
/admin/nft/activity — the full nft_activity feed: mints, transfers, sales,
listings, delistings, bids, offers, burns, collection creation, collection
deployment and auction endings. Rows can be deleted individually with
delete.nft.
This is the audit trail for everything the marketplace did. Deleting from it loses history permanently; prefer filtering.
Disputes
/admin/nft/dispute — not in the navigation, and the only NFT screen that can
move money on its own.
Disputes are created two ways: by the stale-offer sweep when escrow cannot be
released, and by whatever front-end path your install exposes. Seven types are
recognised — FAKE_NFT, COPYRIGHT_INFRINGEMENT, SCAM, NOT_RECEIVED,
WRONG_ITEM, UNAUTHORIZED_SALE, OTHER — across six states: PENDING,
INVESTIGATING, AWAITING_RESPONSE, RESOLVED, REJECTED, ESCALATED.
Priority is a four-rung ordinal: LOW, MEDIUM, HIGH, CRITICAL.
Resolving
Seven resolution types: REFUND, PARTIAL_REFUND, CANCEL_SALE,
REMOVE_LISTING, BAN_USER, WARNING, NO_ACTION. Resolving stamps
resolvedAt and resolvedById and posts a system message into the thread for
the audit trail.
Both credit the buyer's SPOT wallet inside the same database transaction as
the dispute update, under the idempotency key dispute_refund_<disputeId>. This
is not a paper resolution — the money leaves your platform.
The buyer is resolved from the linked sale, falling back to the linked listing,
falling back to the dispute's reporter. REFUND refunds the full original sale
price unless you supply an amount; PARTIAL_REFUND requires one. If the buyer,
the amount or the currency cannot be determined, the whole resolution is
rejected rather than half-applied.
PARTIAL_REFUND is not a value the nft_dispute enum accepts, so it is stored
as REFUND with the real type recorded in the row's metadata. Reporting off
resolutionType alone will undercount partial refunds.
System screens
Analytics
/admin/nft/analytics — volume, revenue, category splits and trends. The totals
are real aggregates. Two decorations are not: the fourteen-point sparkline
trends are generated from the current total times a random factor, and the
category chart falls back to a hardcoded Art / Gaming / Music / Sports quartet
when the real group-by returns nothing. Read the totals; ignore the shapes.
Settings
/admin/nft/settings — five tabs (Trading, Fees, Verification, Content,
Integrations) over seventeen keys. Every key is documented in
Settings.
Onboarding
/admin/nft/onboarding — a four-phase launch checklist. It is not in the
navigation; the Marketplace screen links to it. The phase and task list is a
fixed list in the page; the completion state is read from real data by
/api/nft/onboarding/status.
See Install for what each phase checks.
API-only capabilities
Five admin capabilities have working endpoints and no screen in this build. They are usable through the API with the permission named:
Backups run per chain (ETH, BSC, POLYGON), on an HOURLY, DAILY,
WEEKLY or MONTHLY schedule, as FULL or INCREMENTAL, optionally including
dispute data. The processNFTBackups cron polls every 15 minutes, which is
finer than the tightest schedule, so a due backup fires close to its nextRun.
nft_review.status = 'PENDING' is a real, countable state with no screen behind
it, which is why the moderation dashboard does not publish a count for it — a
queue with no door is a poster.