顧客からの入金と出店者への支払いを混同せず、テスト、照合、自動化できる決済連携を作ります。
1. それぞれの役割を決める
ショップは商品カタログ、出店者アカウント、カート、配送、注文処理を担当します。Wholly Cryptoは決済画面、支払いの検証、出店者への配分、承認済みの送金を担当します。出店者の登録情報はログインアカウントではありません。既存のショップ用プラグインが複数出店者のカートを自動分割するわけではありません。
- 1つの共有カート
- 1つの顧客請求書
- 検証済みの出店者配分
- 承認済みの送金
顧客はまずプロジェクトのウォレットに支払います。鍵を管理し、出店者に渡す資金を預かるのはあなたです。顧客から各出店者への直接分配ではなく、出店者にとっての非カストディ型サービスでもありません。
MarketplaceはBitcoinメインネット、対応するEVMのネイティブコイン、標準ERC-20トークンに対応します。出店者は顧客が支払った資産を同じネットワークで受け取ります。法定通貨への自動換金ではありません。受け取り可能な30チェーンすべてでMarketplace送金ができるわけではありません。
2. 金額を計算する
3人の出店者がそれぞれ100ドルの商品を販売する例です。プロジェクトの手数料を4%に設定し、この例ではストア別や出店者別の上書きは使いません。
| 配分 | 総額 | 運営者の手数料 | 出店者の受取額 |
|---|---|---|---|
| 出店者1人あたり | $100 | $4 | $96 |
| 3人の合計 | $300 | $12 | $288 |
これは請求書で固定されたレートでの相当額で、実際には暗号資産で支払われます。将来のドル価値を保証するものではありません。通常の1%の処理手数料は、この300ドルの請求書に対してプリペイド残高から3ドルを1回だけ使用します。出店者ごとに課金されるわけではありません。この手数料が適用される場合、12ドルの粗手数料収入からネットワーク費用を引く前に9ドルが残ります。
Bitcoinの手数料やEVMガス用に、自由に使えるネイティブコインを別に用意しましょう。出店者の保護された元本を手数料に使ってはいけません。通常のスイープではMarketplaceの受取アドレスの資金を使えません。
3. ウォレットと出店者を準備する
- Project → Marketplace → SettingsでMarketplaceを有効にし、ストアを選び、手数料を設定します。設定中は送金を一時停止し、自動ルールをオフにしておきましょう。
- プロジェクトのウォレットとデータベースをバックアップします。ストアで必要なBTC/EVM決済を有効にし、スキャナーの準備状況を確認します。処理用クレジットと、別枠のネイティブ手数料資金も補充してください。
- 各出店者を追加します。UUIDをショップ側の出店者IDに紐付けて保存し、external_idにその参照を入れられます。送金先アドレスは個別に確認し、正確なチェーンとネットワークに対して承認してください。
各決済方法には、参加するすべての出店者に対応した承認済みの宛先が必要です。後から出店者のアドレスを変更しても、既存の支払い義務の宛先が勝手に変わることはありません。
送金の検証には、正の確認数と互換性のある独立した2つのプロバイダーが必要です。決済画面でゼロ確認やスキャナー1つを許可していても同じです。
4. 共有カートを接続する
Settings → API accessでプロジェクトを限定したMarketplace認証情報を作成します。決済バックエンドにmarketplace.readとinvoices.writeを付与し、必要に応じてストアも限定します。このキーにはアドレス承認や送金承認の権限を含めないでください。
価格、割引、税金、送料はサーバーで計算し、各出店者の取り分に割り当てます。異なる1〜100人の出店者と正の金額を小数文字列で送信します。各総額の合計は請求額と完全に一致する必要があります。ブラウザから来た配分を信用したり、金額計算に浮動小数点を使ったりしないでください。
下の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キーを顧客のブラウザに置かないでください。
SDKを使いたい? Marketplaceの例から始めましょう:
5. 顧客の支払いと出店者への送金を分ける
決済画面から戻ってきただけでは支払いの証明になりません。元の本文でコールバック署名を検証し、時刻と対象プロジェクトを確認し、受信確認を返す前にevent_idを一意に保存します。注文単位の重複防止も設け、再送で二重に注文処理しないようにしましょう。
| イベント | 分かること |
|---|---|
invoice.settled | 顧客の請求書が決済済みになりました。注文処理の前に要確認フラグとMarketplaceの保留を確認してください。 |
marketplace.allocations.available | 検証済みの配分を送金できます。出店者がすでに受け取ったという意味ではありません。 |
marketplace.payout.confirmed | 送金に必要な確認チェックが完了しました。 |
請求書IPNはストアのIPNシークレットを使います。MarketplaceのライフサイクルWebhookにはエンドポイントごとに独自の署名シークレットがあります。受信処理は分けておきましょう。欠落や順序違いのイベントを照合するときは、APIで請求書や送金の現在の状態を取得してください。
6. まず確認してから自動化する
検証済みの配分が利用可能になったら送金の一時停止を解除します。ただし自動ルールはオフのままにします。Marketplace → Payoutsで配分を準備し、受取人、ネイティブ手数料とガスの上限を確認して、正確な計画を1回承認します。BroadcastだけでなくPaidまで追跡してください。
Bitcoinは選んだ請求書の未払い分をすべて同じバッチにまとめます。EVMトークンは送る前にガス補充が必要な場合があります。複数のトランザクションなので、すべて成功またはすべて取り消しになる原子的な分配ではありません。
少額テストが成功したら、資産ごとに自動ルールを設定します。最低額、1回と1日あたりの元本上限、ネイティブ手数料とガスの予算、間隔、確認数を決めましょう。有効化すると追加クリックなしで送金できます。ただしクレジット不足やサーバーの送金制限で支出が停止することはあります。
7. 例外に対応する
不足、遅延、混在した支払い、チェーン再編成
決済完了でも出店者の取り分が全額裏付けられているとは限りません。許容誤差で不足資金は生まれません。保留中の配分を確認し、追加入金を待つか、返金するか、全額確保できる小さい配分を明示的に承認します。過払いが自動的にマーケットプレイスの追加収益になるわけではありません。
送金が止まった、またはリクエストがタイムアウトした
保存済みのトランザクションハッシュ、送信元残高、ガス、確認理由を調べます。既存の送金を再開または照合し、レスポンスを失っただけで別の送金を作らないでください。バックアップ復元後は、チェーン上の結果と台帳が一致するまで送金を停止しておきます。
顧客に返金が必要になった
顧客が管理する返金先アドレスを確認します。対応する返金は同じ資産での全額返金です。全出店者への支払い後はメインウォレットに別途資金を用意します。Wholly Cryptoは出店者から資金を引き落とせません。一部だけ支払ったバッチは手動照合が必要で、部分返金ボタンがあるとは考えないでください。
8. 公開前にチェックする
- 少額のBTCやEVM決済を、出店者への送金確認までテストします。チェーン、トークン契約、宛先、手数料収入、別枠の費用を確認しましょう。
- 重複コールバック、API応答のタイムアウト、不足入金、保留中の送金をテストし、二重の注文処理や二重送金が起きないことを確認します。
- 非公開のバックアップをサーバー外に保管し、送金の失敗を監視し、出店者への支払い義務とチェーン上の資金を定期的に照合しましょう。
少数の出店者と確認付きの送金から始めましょう。テスト済みの資産、予算、宛先だけを自動化してください。