Marketplace purchases, refunds and disputes
The purchase side of the console is read-only and no refund endpoint exists, so this is how a refund is actually issued by hand, what the purchase row tells you to reverse, and which figures disagree afterwards.
The strategy marketplace takes real money from buyers and pays real money to sellers, in one database transaction, at the moment of purchase. "A buyer wants their money back" is therefore a certainty on any install that sells strategies, and it is the one marketplace question the admin console cannot answer.
Not in the admin API, not in the user API, not behind a permission you have not been granted. Every purchase-side admin route is a GET. Nothing in this addon reverses a sale, cancels one, or changes a purchase row's status.
A refund is therefore three hand-made wallet adjustments in core Finance, made by
you, against a purchase row that will still read COMPLETED when you are
finished. Read this whole page before you make the first one — the money left the
buyer in two directions, and refunding only the seller's leg leaves the
platform holding a fee on a sale that did not happen.
What the console gives you
Four endpoints, all read-only, all gated on view.trading_bot.marketplace.
Only two of those are behind a screen. Admin → Trading Bot → Marketplace
(/admin/trading-bot/marketplace) is a table of listings — creator, name,
type, status, visibility, price, risk level, purchases, rating, total revenue —
with four stats cards fed by stats. There is no purchases screen: nothing
in the product renders a list of individual sales, and neither purchases nor
pending is called by any page. To see a purchase row you call the endpoint.
purchases accepts strategyId, buyerId and sellerId as query filters, plus
limit and offset, and returns only rows whose status is COMPLETED, newest
first. That is how you find the transaction a disputing customer is talking
about.
What you can actually do
| Action | Endpoint | Requires |
|---|---|---|
| Approve a pending listing | POST …/marketplace/strategy/{id}/approve |
edit.trading_bot.marketplace; optional notes |
| Reject a pending listing | POST …/reject |
A reason, minimum 10 characters |
| Suspend an approved listing | POST …/suspend |
A reason |
| Reinstate a suspended listing | POST …/reinstate |
edit.trading_bot.marketplace |
| Approve or reject a review | POST /api/admin/trading-bot/reviews/{id}/approve or /reject |
edit.trading_bot.review |
The row menu on the marketplace table offers each action only for the status its
route accepts — approve and reject on PENDING_REVIEW, suspend on APPROVED,
reinstate on SUSPENDED — and reject and suspend open a dialog demanding the
ten-character reason before they will send.
None of these moves money. Suspending a listing stops future sales of it and notifies the seller; it does not touch a single wallet, a single purchase row, or a single bot.
Suspending does not revoke what a buyer bought
This is the question that follows every fraud report, so be clear about it before you suspend anything.
A bot built from a strategy copies the strategy's configuration onto its own
strategyConfig column at creation. The bot does not read the listing again,
ever. So:
- Bots already running from a suspended strategy keep running, unchanged.
- The buyer's purchase row is untouched, so their access is untouched. The deploy
path (
POST /api/trading-bot/marketplace/strategy/{id}/create-bot) checks only that aCOMPLETEDpurchase exists — it does not check the listing's status — so a buyer can still deploy new bots from a suspended strategy through the API. - What the buyer loses is the listing page:
/trading-bot/marketplace/{id}resolves a strategy only when it isAPPROVEDandPUBLIC, or when the caller is its creator, so after suspension the page 404s for everyone else.
If a strategy is actively harmful, suspending it is not containment. Containment
is suspending it and working through the bots built from it —
/admin/trading-bot/bot filtered to that strategy's buyers, force-stopping each
one. See The admin console for what a force stop does and in what
order.
What one sale actually did
Everything you need to reverse is on the purchase row, and nothing on it is derived from today's settings.
| Column | Holds |
|---|---|
price |
What the buyer was debited, in currency |
currency |
The listing's currency — the wallet to work in. Defaults to USDT |
platformFee |
What the platform actually took |
platformFeePercent |
The rate in force at the time of that sale |
sellerAmount |
What the seller was actually credited |
strategySnapshot · strategyVersion |
Name, type, version and the exact config the buyer paid for |
status |
PENDING · COMPLETED · REFUNDED · FAILED |
buyerId · sellerId · strategyId |
Who and what |
createdAt |
When the purchase was recorded |
Three wallet movements happened, all on SPOT wallets in currency, all
inside one transaction:
- The buyer was debited
price— descriptionStrategy purchase: {name}, reference{purchaseId}_buy. - The platform fee was credited to the Super Admin's SPOT wallet and an
adminProfitrow of typeTRADEwas written — descriptionPlatform fee from trading bot strategy purchase: {name}, reference{purchaseId}_fee. It appears on Finance → Revenue Analytics (/admin/finance/profit). - The seller was credited
sellerAmount— descriptionStrategy sale: {name}, reference{purchaseId}_sell.
platformFee and platformFeePercent can legitimately be 0 on a paid sale.
If there was no Super Admin configured, or the buyer was the Super Admin, the
fee could not be routed — so rather than destroy the money the seller is credited
the full price and the row is corrected to record a 0% fee.
Reversing such a sale means taking the whole price back from the seller and
nothing from the platform. Computing the split from tradingBotPlatformFeePercent
instead would leave you demanding a fee the seller never received. A run of 0%
rows also means you are earning nothing on those sales — fix the Super Admin
wallet.
Issuing a refund by hand
Adjustments are written straight to the ledger. There is no approval queue and no reversal button. Confirm the purchase row first, write the amounts down, and do the debits before the credit so you cannot pay a buyer out of platform funds you then fail to recover.
You need edit.wallet for the adjustments and view.trading_bot.marketplace to
read the purchase.
-
Find the purchase. Call
GET /api/admin/trading-bot/marketplace/purchases?buyerId={id}(or?strategyId={id}) and identify the row bycreatedAtandprice. Recordprice,platformFee,sellerAmount,currency,sellerId,buyerIdand the purchaseid. -
Check the money is still there. Go to Finance → Transaction Management → Wallets (
/admin/finance/wallet) and look up the seller's SPOT wallet in that currency. ASUBTRACTis refused outright with "Insufficient funds in wallet" if the balance is below the amount — a seller who has already withdrawn is a collections problem, not an adjustment. -
Debit the seller
sellerAmount. Row menu → Adjust Balance → type Subtract (debit), the amount, and a description naming the purchase id. Turn the Notify user switch off unless you want the seller to receive a bare balance-change email before you have spoken to them. -
Debit the Super Admin
platformFee, if it is non-zero — same screen, the Super Admin's SPOT wallet in the same currency. This is the leg people skip. TheadminProfitrow on/admin/finance/profitstays where it is, so the revenue screen will keep counting that fee either way; taking the balance back at least stops the money being spent twice. -
Credit the buyer
price. Type Add (credit), the same amount the row says they were debited, with a description naming the purchase id. Leave Notify user on for this one — the customer should get the confirmation. -
Decide about access, deliberately. The purchase row still reads
COMPLETED, so the buyer keeps the strategy: it stays in their purchases, they can still deploy bots from it, and they can still review it. Leaving it alone is the right answer for a goodwill refund. Revoking access means writingstatus = 'REFUNDED'on the row directly in the database — there is no endpoint and no screen for it — and that has consequences worth knowing before you do it.
The purchase model declares a unique index on (buyerId, strategyId), and the
purchase path always inserts a new row. So once a buyer has any row for a
strategy — COMPLETED or REFUNDED — a second purchase of that strategy by that
buyer fails on the index. If they later want to buy it properly, the old row has
to be deleted first.
Flipping to REFUNDED also removes the sale from every reader that filters on
COMPLETED: the buyer's My Purchases, their hasPurchased flag, their ability to
deploy from it or review it, the admin purchases list, and the marketplace stats.
It does not decrement the strategy's own counters, so nothing on the seller's
own dashboard changes at all — see below.
If the dispute is broad rather than about one sale, the lever that stops the bleeding while you investigate is Enable Marketplace on Admin → Trading Bot → Settings. Off, it refuses purchases, submissions and reviews and leaves browsing, strategy authoring and every running bot completely untouched. The strategy marketplace has the exact split.
The fee setting, and who may change it
It ships at 10, and 10% is what the purchase path has always actually charged
— the rate was hard-coded there before the setting was wired up, so the default
was chosen to match the behaviour rather than the other way round. The slider on
the Fees tab of /admin/trading-bot/settings runs 0–30 in steps of 1.
There is one percentage and there will not be a second: the seller is paid
price − fee by construction, so a separate creator-payout percentage could only
disagree with it.
tradingBotPlatformFeePercent is in PROTECTED_SETTING_KEYS, along with
tradingBotTradingFeePercent, tradingBotEnabled and
tradingBotLiveTradingEnabled. An admin holding edit.trading_bot.settings — or
edit.settings — but not the Super Admin role gets a 403 naming the
field, and no part of the save is applied.
Both doors enforce it. The settings screen saves through
PUT /api/admin/system/settings, and the addon's own
PUT /api/admin/trading-bot/settings re-applies the same test so it cannot become
a cheaper way onto the platform's fee split and its live-trading kill switch. A
field re-submitted at its current value is not a change and does not trip the
gate.
Changing the rate never re-prices history: the rate in force is stamped onto each
purchase row as platformFeePercent, so a mixed history after a change is
expected.
Why the seller's figure disagrees with yours
Three different things count marketplace money, and after any refund — or any sale where the fee could not be routed — they stop agreeing. This is the source of most "you under-paid me" tickets.
| Where | What it counts | Read from |
|---|---|---|
Seller's headline Total Revenue on /trading-bot/creator (captioned "Earnings from sales") |
The sum of the creatorRevenue column across their strategies |
A running counter, incremented on each sale |
| Revenue on each of the seller's strategy cards, same page | That strategy's own creatorRevenue column |
A running counter |
| Admin Marketplace stats | SUM(price) and SUM(platformFee) over all COMPLETED purchase rows |
The purchase rows |
Admin per-listing Revenue split on /admin/trading-bot/marketplace/{id} |
The totalRevenue, creatorRevenue and platformRevenue columns on that strategy |
Running counters |
The counters — totalPurchases, totalRevenue, creatorRevenue,
platformRevenue — are only ever incremented. Nothing decrements them, because
nothing in the addon reverses a sale. So a refund you make by hand, or a purchase
row you set to REFUNDED, drops out of every row-derived figure and stays in
every counter-derived one. Nothing the seller can see is row-derived, so their
Total Revenue and their per-strategy figures will both keep including a sale
you refunded, and the listing's revenue split will keep claiming platform revenue
you gave back.
Two smaller sources of the same argument, both by design:
- A sale whose fee could not be routed pays the seller the full
priceand recordsplatformFee: 0. Gross taken, seller's cut and platform's cut still add up on that row; the seller's cut is simply all of it. - Free strategies create a
COMPLETEDpurchase row withprice,platformFeeandsellerAmountall zero, and incrementtotalPurchases. They raise the purchase count everywhere without moving any figure that has a currency on it.
When you have to reconcile, the purchase rows are the record and the counters
are a cache. Answer from GET /api/admin/trading-bot/marketplace/purchases, not
from either dashboard.
Related
- The strategy marketplace — the listing lifecycle, the price band, the fee split and the kill switch
- Settings reference — all 27 keys, including both fee percentages
- The admin console — moderation queues, force stop and the audit trail
- Customer wallets and balance adjustments — the adjustment screen in full
- Reading the revenue screen — where the platform's fee lands