← Все уроки

УРОК 11 / 11

Создай свой криптомаркетплейс.

От общей корзины до выплат продавцам: пример на 300 долларов и код для сервера.

Что получится

Платёжная интеграция, которую можно тестировать, сверять и автоматизировать, не путая оплату покупателя с выплатами продавцам.

1. Определи, кто за что отвечает

Твой магазин управляет каталогом, аккаунтами продавцов, корзиной, доставкой и выполнением заказов. Wholly Crypto отвечает за checkout, проверку оплаты, доли продавцов и одобренные выплаты. Запись продавца не даёт ему аккаунт для входа; существующие плагины магазинов не разделяют общую корзину между продавцами автоматически.

  1. Одна общая корзина
  2. Один счёт покупателю
  3. Проверенные доли продавцов
  4. Одобренные выплаты

Сначала покупатели платят на кошельки проекта. Ты контролируешь ключи и хранишь средства, причитающиеся продавцам. Это не прямой перевод от покупателя каждому продавцу и не некастодиальный сервис для твоих продавцов.

Marketplace поддерживает основную сеть Bitcoin, совместимые EVM-монеты и стандартные токены ERC-20. Продавцы получают актив в той же сети, которую использовал покупатель, без автоматической конвертации в фиат. Не все 30 сетей приёма доступны для выплат Marketplace.

2. Рассчитай суммы

Три продавца продают товары на 100 долларов каждый. Установи комиссию проекта 4%, без отдельных настроек магазина или продавца для этого примера.

ДоляДо комиссииТвоя комиссияПолучает продавец
Каждый продавец$100$4$96
Все трое$300$12$288

Это эквиваленты по зафиксированному курсу счёта, выплачиваемые в криптовалюте. Их будущая стоимость в долларах не гарантирована. Обычная комиссия обработки 1% списывает 3 доллара предоплаченного кредита за этот счёт на 300 долларов один раз, а не за каждого продавца. Когда она применяется, от твоих 12 долларов валовой комиссии остаётся 9 до сетевых расходов.

Отдельно держи свободные нативные монеты для комиссий Bitcoin или газа EVM. Комиссии не должны расходовать защищённые суммы продавцов. Обычные sweep-переводы не могут тратить средства с адресов приёма Marketplace.

3. Подготовь кошельки и продавцов

  1. В Project → Marketplace → Settings включи Marketplace, выбери магазины и задай комиссию. На время настройки оставь выплаты на паузе, а автоматические правила выключенными.
  2. Сделай резервные копии кошельков проекта и базы данных. Включи нужные BTC/EVM-методы в магазине, проверь сканеры и пополни кредит обработки и отдельные нативные средства на комиссии.
  3. Добавь каждого продавца. Сохрани его UUID рядом с ID продавца в своём магазине; external_id может хранить эту связь. Независимо проверь и одобри каждый адрес выплаты для точной цепочки и сети.

Для каждого метода checkout у всех участвующих продавцов должны быть подходящие одобренные адреса. Поздняя смена адреса продавца не перенаправляет уже существующие обязательства незаметно.

Для проверки выплат нужны положительное число подтверждений и два независимых совместимых провайдера, даже если checkout допускает ноль подтверждений или один сканер.

Пошаговая настройка в консоли · Резервное копирование и восстановление

4. Подключи общую корзину

Создай ключ Marketplace с доступом к нужному проекту в Settings → API access. Выдай серверу checkout marketplace.read и invoices.write, при необходимости ограничив магазином. Не добавляй к этому ключу права одобрения адресов и выплат.

Считай цены, скидки, налоги и доставку на сервере, затем распределяй их по долям продавцов. Передавай от 1 до 100 разных продавцов с положительными суммами в десятичных строках; суммы до комиссии должны точно совпасть с итогом счёта. Не доверяй распределению из браузера и не используй арифметику с плавающей точкой для денег.

Замени hostname API и UUID-заглушки ниже. Загружай WHOLLY_TOKEN из окружения сервера. Запрос наследует настроенную комиссию 4%; право менять комиссию ему не нужно.

Открой запрос на cURL, JavaScript, PHP или Python
cURL
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
  --request POST \
  --url "https://api.example.com/v1/marketplace/invoices" \
  --header "Authorization: Bearer $WHOLLY_TOKEN" \
  --header 'Idempotency-Key: cart-1042-marketplace-v1' \
  --header 'Content-Type: application/json' \
  --data-raw '{
  "project_id": "YOUR_PROJECT_ID",
  "store_id": "YOUR_STORE_ID",
  "amount": "300.00",
  "currency": "USD",
  "order_id": "cart-1042",
  "description": "One order from three vendors",
  "ipn_url": "https://shop.example.com/payments/wholly-ipn",
  "metadata": {
    "cart_id": "cart-1042"
  },
  "allocations": [
    {
      "vendor_id": "VENDOR_A_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_B_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_C_UUID",
      "gross_amount": "100.00"
    }
  ]
}'
JavaScript
// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
  "project_id": "YOUR_PROJECT_ID",
  "store_id": "YOUR_STORE_ID",
  "amount": "300.00",
  "currency": "USD",
  "order_id": "cart-1042",
  "description": "One order from three vendors",
  "ipn_url": "https://shop.example.com/payments/wholly-ipn",
  "metadata": {
    "cart_id": "cart-1042"
  },
  "allocations": [
    {
      "vendor_id": "VENDOR_A_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_B_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_C_UUID",
      "gross_amount": "100.00"
    }
  ]
}`;
const response = await fetch("https://api.example.com/v1/marketplace/invoices", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Idempotency-Key": "cart-1042-marketplace-v1",
    "Content-Type": "application/json"
  },
  body,
  redirect: "error",
  signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());
PHP
<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
  "project_id": "YOUR_PROJECT_ID",
  "store_id": "YOUR_STORE_ID",
  "amount": "300.00",
  "currency": "USD",
  "order_id": "cart-1042",
  "description": "One order from three vendors",
  "ipn_url": "https://shop.example.com/payments/wholly-ipn",
  "metadata": {
    "cart_id": "cart-1042"
  },
  "allocations": [
    {
      "vendor_id": "VENDOR_A_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_B_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_C_UUID",
      "gross_amount": "100.00"
    }
  ]
}
JSON;
$ch = curl_init("https://api.example.com/v1/marketplace/invoices");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: cart-1042-marketplace-v1", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));
Python
# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler

class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None

# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
    "Idempotency-Key": "cart-1042-marketplace-v1",
    "Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
  "project_id": "YOUR_PROJECT_ID",
  "store_id": "YOUR_STORE_ID",
  "amount": "300.00",
  "currency": "USD",
  "order_id": "cart-1042",
  "description": "One order from three vendors",
  "ipn_url": "https://shop.example.com/payments/wholly-ipn",
  "metadata": {
    "cart_id": "cart-1042"
  },
  "allocations": [
    {
      "vendor_id": "VENDOR_A_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_B_UUID",
      "gross_amount": "100.00"
    },
    {
      "vendor_id": "VENDOR_C_UUID",
      "gross_amount": "100.00"
    }
  ]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/marketplace/invoices",
                  method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
    print(json.load(response))

Сохрани тело запроса и Idempotency-Key до отправки. После тайм-аута повтори то же тело с тем же ключом. Запиши data.invoice_id в заказ и перенаправь покупателя на links.checkout верхнего уровня ответа. Никогда не передавай API-ключи в браузер покупателя.

Где найти UUID проекта и магазина →

Предпочитаешь SDK? Начни с примеров Marketplace:

5. Разделяй оплату и выплаты

Возврат с checkout не доказывает оплату. Проверь подпись callback по исходному телу, время и ожидаемый проект, затем сохрани event_id как уникальный до подтверждения приёма. Добавь отдельную защиту на уровне заказа, чтобы повторы не выполнили его дважды.

СобытиеЧто оно означает
invoice.settledСчёт покупателя оплачен и подтверждён. Перед выполнением заказа проверь флаги проверки и удержания Marketplace.
marketplace.allocations.availableПроверенные доли доступны для выплаты. Это ещё не значит, что кто-то из продавцов получил деньги.
marketplace.payout.confirmedВыплата прошла проверки подтверждений.

IPN счетов использует IPN-секрет магазина. Webhook-события Marketplace используют свой секрет подписи для каждого endpoint. Держи обработчики раздельными. При сверке пропущенных или перепутанных событий считай текущее состояние счёта или выплаты через API.

Callback-события счетов и проверка подписи · События Marketplace и справочник API

6. Сначала проверь, потом автоматизируй

Когда проверенные доли станут доступны, сними паузу выплат, но оставь автоматические правила выключенными. В Marketplace → Payouts подготовь распределение, проверь получателей и лимиты нативных комиссий и газа, затем один раз одобри точный план. Дождись Paid, а не только Broadcast.

Bitcoin объединяет все неоплаченные доли выбранного счёта. Для EVM-токенов перед переводом может понадобиться пополнение газа. Это несколько транзакций, а не атомарная операция «всё или ничего».

После успешного небольшого теста задай автоматическое правило для каждого актива: минимум, лимиты основной суммы на выплату и сутки, бюджеты нативных комиссий и газа, интервал и подтверждения. Включение разрешает отправку без новых кликов. Недостаток кредита или выключенный допуск переводов на сервере всё ещё могут остановить расходование.

7. Разберись со сложными случаями

Недоплаты, поздние, смешанные платежи и реорганизации

Подтверждённый checkout не гарантирует полное обеспечение долей продавцов. Допуск недоплаты не создаёт недостающие деньги. Проверь удержанные доли, дождись доплаты, верни платёж или явно одобри меньшую, полностью обеспеченную сумму. Переплата не становится автоматически дополнительным доходом маркетплейса.

Выплата застряла или запрос завершился тайм-аутом

Проверь сохранённые хеши транзакций, исходные балансы, газ и причину проверки. Продолжи или сверь существующую выплату; не создавай второй перевод из-за потери ответа. После восстановления держи выплаты на паузе, пока результаты в сети и журнал не совпадут.

Покупателю нужен возврат

Проверь адрес возврата, которым управляет покупатель. Поддерживаются полные возвраты в том же активе. Если все продавцы уже получили деньги, отдельно пополни основной кошелёк: Wholly Crypto не может списать их обратно. Частично оплаченная группа продавцов требует ручной сверки, а не предполагаемой кнопки частичного возврата.

8. Проверь всё перед запуском

  • Проведи небольшой платёж BTC и/или EVM до подтверждённых выплат продавцам. Проверь сеть, контракт токена, адреса, комиссию и отдельные расходы.
  • Проверь повторный callback, тайм-аут API, недоплату и удержанную выплату. Убедись, что они не вызывают повторное выполнение заказа или двойной перевод.
  • Храни приватные резервные копии вне сервера, следи за сбоями выплат и регулярно сверяй обязательства перед продавцами со средствами в сети.

Начни с проверяемых вручную выплат и небольшого числа продавцов. Автоматизируй только протестированные активы, бюджеты и адреса.