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.

10 min readUpdated 6 August 2026marketplace, refunds, purchases, fees, disputes

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.

All listings, any status
The review queue, oldest submission first
Every completed purchase, with its fee breakdown
Marketplace counts and revenue totals

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 a COMPLETED purchase 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 is APPROVED and PUBLIC, 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:

  1. The buyer was debited price — description Strategy purchase: {name}, reference {purchaseId}_buy.
  2. The platform fee was credited to the Super Admin's SPOT wallet and an adminProfit row of type TRADE was written — description Platform fee from trading bot strategy purchase: {name}, reference {purchaseId}_fee. It appears on Finance → Revenue Analytics (/admin/finance/profit).
  3. The seller was credited sellerAmount — description Strategy 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.

  1. Find the purchase. Call GET /api/admin/trading-bot/marketplace/purchases?buyerId={id} (or ?strategyId={id}) and identify the row by createdAt and price. Record price, platformFee, sellerAmount, currency, sellerId, buyerId and the purchase id.

  2. 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. A SUBTRACT is 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.

  3. 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.

  4. 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. The adminProfit row on /admin/finance/profit stays 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.

  5. 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.

  6. 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 writing status = '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

The platform's cut of every marketplace sale. The seller receives the residual

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 price and records platformFee: 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 COMPLETED purchase row with price, platformFee and sellerAmount all zero, and increment totalPurchases. 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.