Build a marketplace#
You have sellers. They have things agents will pay for. You want to run the storefront, take a cut, and never touch anyone's money.
That is a marketplace on mechawallet — the API calls it platform mode
(platform_account, platform_fee_*), and this guide uses both words for
the same thing: one account (yours), one API key, and
one checkout per thing your sellers sell — with their wallet as the payout
and your wallet on the fee. Settlement pays everyone in the same
transaction, on-chain. There are no sub-accounts to create, no balances to
hold, no payouts to schedule, and nothing for your sellers to sign up for
here — a seller is a wallet address on a checkout, full stop.
buyer ──pays $50.26──▶ settlement ──▶ $49.00 seller's wallet
├──────▶ $1.00 your wallet (2% platform fee)
└──────▶ $0.26 protocol fee
The legs always sum to what the buyer paid — settlement is disbursement, so there is no rounding gap for money to hide in. The buyer pays exactly what a $50 checkout without a platform costs: $50 plus the protocol fee ($0.01 + 0.5%, charged on top). Your cut comes out of the seller's side — platform prices never drift from non-platform prices — and every receipt itemizes it, so your sellers always see the deal they took.
gitbuyer is this guide, running: repos for sale, sellers paid per clone, one mechawallet account underneath. When a step below feels abstract, go click around it.
Set up, from zero#
The whole onboarding is six calls, in this order, and every one is safe to re-run. Nothing below assumes an existing account — this is the sequence a brand-new marketplace (or a deploy script rebuilding one) follows.
0 — An account. Two doors, both end holding a credential that can do everything below:
- Agent-native:
POST /auth/wallet/challenge→ sign it →POST /auth/wallet/verify→ your first full-scope key, minted on the spot (the signing wallet is registered as your verified payout wallet, so step 1 is already done). - Email: request a magic link,
POST /auth/verify→ the response carries a bearer session token that works inAuthorization: Bearer …on every endpoint below — the dashboard is optional, not required.
1 — Your payout wallets: this is where your cut lands. One wallet per chain, and this is the account's ONLY wallet store — an ordinary seller's sales land here; a marketplace's cut lands here. Set one for EACH chain your sellers sell on: a chain without a wallet is dropped from your fee-carrying checkouts, and its buyers with it. Also required before any key can be minted.
curl -s -X PUT https://api.mechawallet.com/v1/account/wallets \
-H "Authorization: Bearer $SESSION_OR_KEY" \
-H "content-type: application/json" \
-d '{"wallets": {"evm": "0xYourBase…", "svm": "YourSolana…"}}'
2 — Marketplace mode + your standing fee. One call, the whole B2B2X switch:
curl -s -X PUT https://api.mechawallet.com/v1/account/platform \
-H "Authorization: Bearer $SESSION_OR_KEY" \
-H "content-type: application/json" \
-d '{"platform_account": true, "platform_fee_bps": 200}'
The mode and the RATE — there are no wallet fields here, on purpose. Your
cut lands in the payout wallets from step 1, per chain; the response's
fee_wallets shows the resolved view so a missing chain is visible now,
not discovered in lost buyers. Marketplace is a strong either: with the
mode on, every checkout you create is for a sub-seller — creating for the
account itself is refused (sell your own products from a separate seller
account).
(Or the dashboard: Payout wallets → Marketplace.) From this moment:
creates default to inherit_account_payouts: false (your wallets can never
leak onto a sub-seller's checkout), the standing fee rides on every
sub-seller create that doesn't name its own, and whoami / /auth/me
report the configuration so your service can assert it at boot.
3 — Your key(s). POST /v1/keys — one is enough; several with their
own fee tiers if you run several storefronts (next section).
4 — Sale events. Tell mechawallet where to deliver — re-runnable by design (the same POST is setup, recovery and revocation in one):
curl -s -X POST https://api.mechawallet.com/v1/webhooks \
-H "Authorization: Bearer $FULL_KEY_OR_SESSION" \
-H "content-type: application/json" \
-d '{"url": "https://yourservice.example/internal/mw-events"}'
Your receiver needs no configured secret. Verify each delivery's
X-Mechawallet-Ed25519 header against the public key at
GET /v1/webhooks/verification-key (no auth — fetch it at boot). The API
key is the only credential your deploy requires. The response's secret
exists for receivers that prefer symmetric HMAC verification instead
(X-Mechawallet-Signature) — re-POSTing the same URL re-keys it
("rekeyed": true), and DELETE /v1/webhooks/{id} removes a receiver.
See Webhooks for both verification paths.
5 — Sell. Create one checkout per thing your sellers sell (next
sections). whoami at boot confirms steps 1–4 took.
One rate per key (several storefronts, one account)#
Running more than one storefront or seller tier? Give each its own key with its own RATE — the credential is the tier. A rate, never a wallet: keys name how much, the account's payout wallets name where.
curl -s -X POST https://api.mechawallet.com/v1/keys \
-H "Authorization: Bearer $YOUR_KEY" \
-d '{"scope": "full", "mode": "live", "name": "partners",
"platform_fee_bps": 100}'
Checkouts that key creates carry its rate; keys without one fall
through to the account's standing rate. Precedence, always: explicit
per-create → the key's rate → the standing rate. The rate is echoed in the
mint response, the key list and whoami (key_platform_fee_bps), so a
rotation that lost it is visible immediately — not in next month's margin.
The five primitives#
Everything a platform does is these five. There is deliberately no sixth.
- Your account in platform mode, and its key(s). You authenticate; your sellers never do.
- The checkout is the sub-merchant. Each one carries the seller's
wallet(s), the price, your fee — and
platform_checkout: true, frozen at create, so "yours" and "your sellers'" stay separable everywhere (?platform=true|falseon lists and receipts). - The platform fee — a RATE (per create, per key, or the account's standing one; most specific wins), landing in your payout wallets per chain.
- The per-seller ledger —
GET /v1/receipts?checkout_id=…. - The event feed — one
checkout.paidwebhook; route bycheckout_id.
Create a checkout for a seller#
curl -s -X POST https://api.mechawallet.com/v1/checkouts \
-H "Authorization: Bearer $YOUR_KEY" \
-H "content-type: application/json" \
-d '{
"title": "Clone acme/private-sdk",
"price_usd": "50.00",
"pay_to": "0xSELLER…",
"payouts": {"SVM": "SellerSolanaAddr…"},
"fulfillment": "relay",
"origin_url": "https://…"
}'
One line in that body is the platform discipline:
pay_to(+payouts) is the seller's wallet. Money never routes through you. Every rail the seller can receive on is offered to the buyer — at the SAME dollar price, with the checkout itself guiding the buyer to the right one ("Pay with USDC on Base/Solana", installed wallets detected and named, the payable rail leading). Two chains cost you and your sellers zero explanation; they only widen who can pay.
Everything else the mode already did: inherit_account_payouts defaulted to
false (your wallets stay off the seller's rails), and your standing fee
rode on. Your cut needs no line at all. To state it per checkout anyway,
send platform_fee_bps — explicit beats the key's rate and the standing
rate, and "platform_fee_bps": 0 makes that one checkout free of your fee.
There is no selling your own things from a marketplace account: the
strong either refuses it. Use a separate seller account for your own
products.
Know your numbers before you set them#
curl -s "https://api.mechawallet.com/v1/platform/quote?price_usd=50.00&platform_fee_bps=250"
{
"buyer_pays": "50.26",
"seller_receives": "48.75",
"platform_cut": "1.25",
"protocol_fee": "0.26"
}
Public, no account needed — price your platform before you build it. The dashboard's platform section has the same calculator live, and both are computed by the very modules that settle real payments, so the preview and the payment cannot disagree. It exists to make two rules unmissable: your cut plus the seller's take equals the price to the cent, and the protocol fee is paid by the buyer on top — never out of your cut.
The fee rules#
- Capped at 2000 bps (20%). Past that, a "fee" is a revenue share wearing the wrong name, and the API refuses it.
- Taken from the seller's side, floored in the seller's favor.
platform = price × bps ÷ 10000, rounded down. The buyer's total and the protocol fee (0.5% + $0.01, on top, yours to pay never) are untouched. - The cut lands in your payout wallets, per chain — intersected with each seller's chains. Each rail pays your wallet on its own chain. A chain where YOU have no wallet is omitted from that checkout's live face (its buyers included); a chain the SELLER doesn't sell on simply carries no leg — a Base-only seller lists fine on a two-chain marketplace. Only zero overlap refuses (the cut would have nowhere to land), and the error names both fixes. One wallet on each chain and nothing is ever dropped. (EVM fee legs settle through splitter v2; create refuses the leg with the exact fix when v2 is not configured.)
- Itemized everywhere money is shown — receipts, exports, webhooks, the buyer's receipt, the pay page. Your sellers see the cut; that is a feature, not a leak.
- Frozen at create, like everything else on a checkout. Changing the standing fee changes what you create next, never a deal a buyer can already see. Every change to it is written to your account timeline, old and new, attributed.
Receipts: minted by settlement, distributed by you#
You never create a receipt — settlement mints it. Every sale on your
marketplace automatically produces an unforgeable receipt carrying the
on-chain tx_hash, your cut itemized in its disbursements, and a permanent
public page at /r/{payment_id} (plus a printable download and by-tx
recovery). A receipt nobody can POST into existence is a receipt everyone can
trust — that is the design, not a missing endpoint.
What you get per sale, with nothing to derive:
- the
checkout.paidwebhook arrives carryingreceipt_url(relay it straight to your buyer) andsold_by(route the sale to the right sub-seller, no lookup); GET /v1/receipts?sold_by=<seller>is one sub-seller's statement in one call — rows carrysold_by, and the CSV export (/v1/receipts/export) has it as a column for the accountant's subtotals;- receipt pages embed in your own site with the same two-line script as
checkouts —
<div data-mw-receipt="pay_…"></div>on your order page shows the buyer their receipt (and their escrow controls) without leaving your site, under the sameembed_originsallowlist. Receipts also name the parties: your account as seller of record (linked profile, verified badge) andsold_by— "on behalf of" — on every platform sale.
Name the parties, and the receipt speaks your language. The wallets
prove what happened; parties declares who it happened between — in your
namespace, not the chain's:
"parties": {
"buyer": {"name": "Klaus K.", "email": "klaus@acme.io", "ref": "cus_4821"},
"seller": {"name": "Acme Parser Co.", "ref": "sel_77"}
}
Pass it on any create (everything optional, graceful when absent). The
receipt page and printable download then read "Billed to Klaus K." and
"Sold by Acme Parser Co. — on behalf of acme/parser"; the checkout.paid
webhook and the ledger rows echo the whole object (so your refs come back
with the money event); the CSV export carries buyer_name/buyer_ref for
the accountant. mechawallet verifies none of it — you assert, we carry, the
chain proves — and what you put here appears on the receipt page, so share
its URL accordingly. Escrow twins inherit it with every other frozen term.
A decision worth knowing: your sub-sellers do not pull receipts from
mechawallet directly — under the one-account B2B2X model there is nothing for
them to log into, and you are the distributor of statements. The public
capability URL is what you hand onward; the sold_by filter is what you
build their dashboards from. (Scoped per-sub-seller read keys would be a
later product, if ever needed.)
Escrow on your marketplace: you are the judge#
If you offer escrow on sub-seller checkouts, understand one rule first: your marketplace's disputes are yours to decide — mechawallet never arbitrates a checkout you created for one of your sellers. That is what makes B2B2X real: your marketplace, your dispute policy, your ruling.
Concretely:
- Name your arbiter wallets before offering escrow —
PUT /v1/account/escrowwithescrow_arbiter_evmand/orescrow_arbiter_svm(dashboard: Who settles disputes). An escrow create for a sub-seller with no arbiter of yours is refused with a 400; a chain you name no arbiter for simply offers no escrow on your checkouts. The one-word setup:{"arbiter": "payouts"}copies your payout wallets as arbiters — values frozen at that moment, never a live link, so rotating payouts later moves nothing. Still an explicit act, because becoming the judge of your marketplace's disputes should be said, not inherited. - Choose a wallet that can actually rule. Three requirements, learned
the hard way: it must be a key you can sign with (an exchange deposit
address can hold funds and still never rule); on Base it needs a little
ETH, because a ruling is a transaction (Solana rulings ride our
relayer's gas); and once set, keep the key signable until the last
escrow that froze it resolves.
GET /v1/account/escrow/readiness— the dashboard's Check balance button — answers "can I rule today, and roughly how many times" per chain, and the same answer arrives inside everyescrow.escalatedwebhook asarbiter_readiness, so the moment you learn about a dispute you also learn whether you can sign its ruling. - Your arbiter can only ever pick between the two exits the buyer's deposit committed to — release to the seller or refund to the buyer. No third route exists, for you, for us, for anyone.
- Disputes reach you as
escrow.escalatedwebhooks and sit inGET /v1/escrows?status=escalated. Rule from your dashboard's Live escrows panel (both chains — Base signs with your browser wallet, Solana with any Wallet-Standard wallet, gasless), from the API (POST /v1/escrows/{id}/resolve), or let an agent work the queue via MCP (resolve_escrow). - If you never rule, the buyer is refunded automatically at the backstop deadline. Nobody's money locks — but your seller loses by default, so an unworked dispute queue is a policy, whether you meant it or not.
Timing is yours per checkout (presets instant / standard / shipping,
or explicit windows). Details: Escrow.
When a seller changes something#
A checkout's deal — price, wallets, fee — is frozen at creation, which is
what makes a signed payment trustworthy. To change anything: create a new
checkout with the new terms, point your store page at it, and void the old
one (DELETE /v1/checkouts/{id}) so a cached link cannot keep selling at
yesterday's price to yesterday's wallet. Do both, always, in that order.
Per-seller sales, without a ledger of your own#
curl -s "https://api.mechawallet.com/v1/receipts?checkout_id=chk_…" \
-H "Authorization: Bearer $YOUR_KEY"
Rows carry amounts, the payer, the tx hash, platform_amount (your cut) and
test_mode — and every row carries disbursements, the leg-by-leg record a
reconciler wants:
"disbursements": [
{"role": "seller", "wallet": "0xSELLER…", "amount": 4875000},
{"role": "platform", "wallet": "YourAddr…", "amount": 125000},
{"role": "protocol", "wallet": null, "amount": 35000}
]
All legs land in the same transaction (tx_hash): settlement is
disbursement, so there is no later payout run whose status could diverge
from the sale's, and the legs sum to what the buyer paid. Filter one
checkout to answer "what did this seller earn"; subscribe one webhook and
route checkout.paid events by checkout_id to tell them the moment it
happens. Keep rehearsal money out of revenue numbers — rows say test_mode
for exactly that reason.
Attach a payment to YOUR buyer: the handoff nonce#
An x402 buyer is a wallet. Your platform's buyer is an account — and the gap between those two is the last thing standing between "an agent paid a URL" and "my user bought this". The handoff nonce closes it.
The mechanics, end to end:
- Your signed-in buyer asks to buy (or their agent does, holding their
session). You mint a nonce — any
[A-Za-z0-9_-]{16,64}string — and store(nonce, account, checkout)on your side. - You hand out the pay URL with
?h=<nonce>appended. Any face works: plain, escrow,/test. Whoever ends up paying — an agent, a phone, acurl— pays the URL exactly as given. - Settlement stores the nonce on the payment, and you hear it back twice:
-
handoff_idon thecheckout.paidwebhook (and every escrow event). Match it against your table and the order is attached the moment the event arrives. -GET /c/{id}/handoff/{nonce}(also/e/), a public poll that answers 404 until a settled — or escrow-held — payment carries the nonce, then{payment_id, receipt_url, escrow_state, test_mode}. This is what a live browser page uses to react in real time; a wrong guess is indistinguishable from nonexistence.
The hosted pay page cooperates without any work from you: a wallet payment
forwards the page's ?h= into settlement, and the cross-device QR threads
an existing tag into the code it shows, so a buyer who opens your tagged
link on a desktop and pays with their phone keeps the attribution.
Rules worth copying from the platforms already running this:
- The tag is a bearer capability. Whoever pays through it buys for that account. That is the feature — handing the link away only donates a purchase to the minter. Money routing is untouched; the nonce decides which account the goods-side lands on, nothing else.
- Scope it to one checkout and consume it on first settlement. A reusable checkout's second payer is a stranger with a forwarded link.
- Give it an expiry (days, not minutes — agents shop slowly).
- Escrow attaches at
held, revenue waits for release. The nonce tells you whose hold it is; it never makes a hold a sale. - Keep a fallback. A payer that strips the query string settles fine
and attributes to nobody; let buyers claim by
payment_idafter the fact.
Test first, live when you mean it#
A mw_test_ key makes your whole platform a rehearsal: every flow works,
every split is simulated and recorded, no money exists. Swap to a
mw_live_ key and listings sell for real the moment they are created —
so decide first how you prove a seller controls what they are selling
(gitbuyer's answer: a fine-grained GitHub token that can push to the repo).
mechawallet enforces your protocol; vouching for your sellers is your half
of the deal.
The pitch, compressed: your sellers get paid on-chain per sale with no
account here; you get your cut in the same transaction with no money ever in
your custody; buyers — human or agent — pay one URL. One account, one mode
(PUT /v1/account/platform), one standing fee, and the contract underneath
serves every platform at once: nothing about yours is deployed anywhere.