← บทสอนทั้งหมด

บทสอน 11 / 12

สร้างตลาดคริปโตของคุณ

จากตะกร้าร่วมสู่จ่ายเงินให้ผู้ขาย พร้อมตัวอย่าง $300 ที่ใช้ได้จริงและโค้ดฝั่งเซิร์ฟเวอร์

สิ่งที่จะได้

ระบบรับชำระเงินที่ทดสอบ กระทบยอด และทำงานอัตโนมัติได้ โดยไม่สับสนระหว่างเงินที่ลูกค้าจ่ายกับเงินที่จ่ายให้ผู้ขาย

1. แบ่งหน้าที่ให้ชัดเจน

ร้านของคุณดูแลแค็ตตาล็อก บัญชีผู้ขาย ตะกร้า การจัดส่ง และการดำเนินการตามคำสั่งซื้อ ส่วน Wholly Crypto ดูแลหน้าชำระเงิน การตรวจสอบการชำระเงิน การแบ่งยอดให้ผู้ขาย และการจ่ายเงินที่อนุมัติแล้ว รายการผู้ขายไม่ใช่บัญชีเข้าสู่ระบบผู้ขาย และปลั๊กอินร้านค้าที่มีอยู่ไม่ได้แบ่งตะกร้าหลายผู้ขายให้อัตโนมัติ

  1. ตะกร้ารวมเดียว
  2. ใบแจ้งหนี้ลูกค้าใบเดียว
  3. ส่วนแบ่งผู้ขายที่ตรวจสอบแล้ว
  4. การจ่ายเงินที่อนุมัติแล้ว

ลูกค้าจ่ายเข้ากระเป๋าของโปรเจกต์ก่อน คุณควบคุมคีย์และถือเงินที่ต้องจ่ายให้ผู้ขาย นี่ไม่ใช่การแบ่งเงินตรงจากลูกค้าไปยังผู้ขาย และไม่ใช่บริการแบบไม่รับฝากเงินสำหรับผู้ขายของคุณ

Marketplace รองรับ Bitcoin mainnet และเหรียญ EVM กับโทเคน ERC-20 มาตรฐานที่รองรับ ผู้ขายจะได้รับสินทรัพย์บนเครือข่ายที่ลูกค้าใช้จ่าย ไม่ได้แปลงเป็นเงินทั่วไปให้อัตโนมัติ การจ่ายเงิน Marketplace ไม่ได้รองรับครบทั้ง 30 เครือข่ายที่ใช้รับเงิน

2. คำนวณยอดเงิน

ผู้ขายสามรายขายสินค้ารายละ $100 ในตัวอย่างนี้ ตั้งค่าคอมมิชชันของโปรเจกต์เป็น 4% โดยไม่มีการตั้งค่าทับระดับร้านหรือผู้ขาย

ส่วนแบ่งยอดรวมก่อนหักคอมมิชชันของคุณผู้ขายได้รับ
ผู้ขายแต่ละราย$100$4$96
รวมทั้งสามราย$300$12$288

ยอดเหล่านี้เป็นมูลค่าเทียบเท่าตามราคาที่ล็อกไว้ในใบแจ้งหนี้ โดยจ่ายเป็นคริปโต ไม่ใช่การรับประกันมูลค่าดอลลาร์ในอนาคต ค่าประมวลผลปกติ 1% ใช้เครดิตเติมล่วงหน้า $3 สำหรับใบแจ้งหนี้ $300 นี้เพียงครั้งเดียว ไม่ใช่ครั้งละหนึ่งผู้ขาย เมื่อมีค่าธรรมเนียมนี้ คอมมิชชันรวม $12 ของคุณจะเหลือ $9 ก่อนหักค่าเครือข่าย

เก็บเหรียญหลักที่ยังไม่ติดภาระไว้ต่างหากสำหรับค่าธรรมเนียม Bitcoin หรือ gas ของ EVM ค่าธรรมเนียมต้องไม่กินเงินต้นของผู้ขายที่กันไว้ การ sweep ปกติใช้เงินจากที่อยู่รับเงิน Marketplace ไม่ได้

3. เตรียมกระเป๋าและผู้ขาย

  1. ในโปรเจกต์ → Marketplace → การตั้งค่า ให้เปิด Marketplace เลือกร้าน และตั้งค่าคอมมิชชัน ระหว่างตั้งค่าให้พักการจ่ายเงินและปิดนโยบายอัตโนมัติไว้ก่อน
  2. สำรองกระเป๋าของโปรเจกต์และฐานข้อมูล เปิดวิธีรับเงิน BTC/EVM ที่ต้องการในร้าน ตรวจสอบความพร้อมของสแกนเนอร์ และเติมเครดิตประมวลผลพร้อมเหรียญหลักสำหรับค่าธรรมเนียมแยกต่างหาก
  3. เพิ่มผู้ขายแต่ละราย เก็บ UUID คู่กับ ID ผู้ขายในร้านของคุณ โดยใช้ external_id เก็บรหัสอ้างอิงนั้นได้ ตรวจสอบและอนุมัติที่อยู่รับเงินแต่ละแห่งโดยอิสระให้ตรงทั้งเชนและเครือข่าย

แต่ละวิธีชำระเงินต้องมีปลายทางที่ตรงกันและอนุมัติแล้วสำหรับผู้ขายทุกรายที่ร่วมรายการ การเปลี่ยนที่อยู่ผู้ขายภายหลังจะไม่เปลี่ยนปลายทางของยอดค้างจ่ายเดิมแบบเงียบ ๆ

การตรวจสอบการจ่ายผู้ขายต้องมีการยืนยันมากกว่าศูนย์และผู้ให้บริการอิสระที่เข้ากันได้สองราย แม้หน้าชำระเงินจะอนุญาตให้ใช้ศูนย์การยืนยันหรือสแกนเนอร์เพียงรายเดียวก็ตาม

ขั้นตอนตั้งค่าในคอนโซล · สำรองและกู้คืน

4. เชื่อมต่อตะกร้ารวม

สร้างข้อมูลรับรอง Marketplace ที่จำกัดเฉพาะโปรเจกต์ในตั้งค่า → การเข้าถึง API ให้ backend ชำระเงินมีสิทธิ์ marketplace.read และ invoices.write พร้อมจำกัดร้านตามความเหมาะสม อย่าใส่สิทธิ์อนุมัติที่อยู่และอนุมัติจ่ายเงินในคีย์นี้

คำนวณราคา ส่วนลด ภาษี และค่าจัดส่งบนเซิร์ฟเวอร์ของคุณ แล้วจัดสรรเป็นส่วนแบ่งผู้ขาย ส่งผู้ขายไม่ซ้ำกัน 1–100 ราย พร้อมยอดบวกในรูปสตริงทศนิยม โดยยอดรวมก่อนหักของทุกรายต้องเท่ากับใบแจ้งหนี้พอดี อย่าเชื่อส่วนแบ่งจากเบราว์เซอร์หรือคำนวณเงินด้วยเลขทศนิยมแบบ floating point

แทนชื่อโฮสต์ API และ UUID ตัวอย่างด้านล่าง โหลด WHOLLY_TOKEN จาก environment ของเซิร์ฟเวอร์ คำขอนี้ใช้คอมมิชชัน 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))

บันทึก body ของคำขอและ Idempotency-Key ก่อนส่ง หากหมดเวลา ให้ลอง body เดิมด้วยคีย์เดิม เก็บ data.invoice_id คู่กับคำสั่งซื้อ และส่งลูกค้าไปที่ links.checkout ระดับบนสุด อย่าใส่คีย์ API ในเบราว์เซอร์ของลูกค้า

หา UUID ของโปรเจกต์และร้านได้ที่ไหน →

อยากใช้ SDK? เริ่มจากตัวอย่าง Marketplace:

5. ติดตามการรับเงินและการจ่ายผู้ขายแยกกัน

URL กลับจากหน้าชำระเงินไม่ใช่หลักฐานการชำระ ตรวจสอบลายเซ็น callback จาก raw body ตรวจเวลาและโปรเจกต์ที่คาดไว้ แล้วเก็บ event_id แบบไม่ซ้ำก่อนตอบรับ ใช้ตัวป้องกันระดับคำสั่งซื้อแยกต่างหากเพื่อไม่ให้การลองซ้ำทำคำสั่งซื้อซ้ำสองครั้ง

เหตุการณ์บอกอะไรคุณ
invoice.settledใบแจ้งหนี้ของลูกค้าชำระเสร็จแล้ว ตรวจธงตรวจสอบและยอดที่ Marketplace พักไว้ก่อนดำเนินการตามคำสั่งซื้อ
marketplace.allocations.availableส่วนแบ่งที่ตรวจสอบแล้วพร้อมจ่าย แต่ไม่ได้หมายความว่ามีผู้ขายได้รับเงินแล้ว
marketplace.payout.confirmedการจ่ายเงินผ่านการตรวจยืนยันครบแล้ว

IPN ของใบแจ้งหนี้ใช้ IPN secret ของร้าน ส่วน webhook วงจร Marketplace ใช้ signing secret ของ endpoint เอง แยกตัวรับทั้งสองให้ชัดเจน เมื่อต้องกระทบยอดเหตุการณ์ที่ขาดหายหรือมาผิดลำดับ ให้อ่านใบแจ้งหนี้หรือการจ่ายเงินปัจจุบันผ่าน API

Callback ใบแจ้งหนี้และการตรวจสอบลายเซ็น · เหตุการณ์ Marketplace และคู่มือ API

6. ตรวจสอบก่อน แล้วค่อยเปิดอัตโนมัติ

เมื่อส่วนแบ่งที่ตรวจสอบแล้วพร้อมจ่าย ให้เลิกพักการจ่ายเงินแต่ยังปิดนโยบายอัตโนมัติไว้ ใน Marketplace → การจ่ายเงิน เตรียมส่วนแบ่ง ตรวจผู้รับและเพดานค่าธรรมเนียม/gas ของเหรียญหลัก แล้วอนุมัติแผนที่แน่นอนนั้นครั้งเดียว ติดตามจนถึงจ่ายแล้ว ไม่ใช่แค่ส่งเข้าบล็อกเชนแล้ว

Bitcoin รวมทุกส่วนแบ่งที่ยังไม่จ่ายของใบแจ้งหนี้ที่เลือกเป็นชุดเดียว การจ่ายโทเคน EVM อาจต้องเติม gas ก่อนโอนโทเคน จึงเป็นหลายธุรกรรม ไม่ใช่การแบ่งแบบสำเร็จทั้งหมดหรือไม่ทำอะไรเลยในธุรกรรมเดียว

หลังทดสอบยอดเล็กสำเร็จ ให้ตั้งนโยบายอัตโนมัติหนึ่งชุดต่อสินทรัพย์: ยอดจ่ายขั้นต่ำ เพดานเงินต้นต่อครั้งและต่อวัน งบค่าธรรมเนียม/gas รอบเวลา และจำนวนการยืนยัน การเปิดใช้งานเป็นการอนุญาตให้ส่งโดยไม่ต้องกดเพิ่ม แต่เครดิตต่ำหรือการปิดสิทธิ์โอนของเซิร์ฟเวอร์ยังพักการจ่ายได้

7. จัดการกรณีที่ต้องระวัง

จ่ายขาด จ่ายช้า จ่ายหลายแบบ หรือบล็อกเชนจัดลำดับใหม่

หน้าชำระเงินที่เสร็จแล้วไม่ได้รับประกันว่ามีเงินครบสำหรับส่วนแบ่งผู้ขาย ค่าคลาดเคลื่อนสร้างเงินที่ขาดไม่ได้ ตรวจส่วนแบ่งที่ถูกพัก รอรับเงิน คืนเงิน หรืออนุมัติส่วนแบ่งที่น้อยลงแต่มีเงินจริงรองรับอย่างชัดเจน เงินที่จ่ายเกินไม่กลายเป็นรายได้เพิ่มของ Marketplace โดยอัตโนมัติ

การจ่ายเกินไม่บล็อกจ่ายผู้ขายตามราคาเสนอบน Bitcoin เหรียญ EVM หรือ ERC-20 ที่รองรับ ส่วนผู้ขายคงเดิมและส่วนเกินแยกไว้กระทบยอด

การจ่ายค้างหรือคำขอหมดเวลา

ตรวจแฮชธุรกรรมที่บันทึกไว้ ยอดต้นทาง gas และเหตุผลที่ต้องตรวจสอบ ทำต่อหรือกระทบยอดการจ่ายเดิม อย่าสร้างการโอนครั้งที่สองเพียงเพราะคำตอบหาย หลังคืนค่าจาก backup ให้พักการจ่ายไว้จนผลบนเชนตรงกับบัญชีรายการ

การทำต่อแทนธุรกรรม EVM ที่ล้มเหลวได้ก็ต่อเมื่อผู้ให้บริการอิสระสองรายยืนยันว่าธุรกรรมบนเชน revert แล้ว ผลที่ยังไม่ทราบจะคงการจองเงินไว้ และค่าธรรมเนียมจากครั้งที่ล้มเหลวยังนับในงบเดิม

ลูกค้าต้องการคืนเงิน

ตรวจว่าที่อยู่คืนเงินเป็นของลูกค้า การคืนเงินที่รองรับเป็นยอดเต็มในสินทรัพย์เดิม หลังจ่ายผู้ขายครบแล้วต้องเติมเงินที่กระเป๋าหลักต่างหาก Wholly Crypto ดึงเงินกลับจากผู้ขายไม่ได้ ชุดที่จ่ายผู้ขายไปบางรายต้องกระทบยอดด้วยตนเอง ไม่ใช่ถือว่ามีปุ่มคืนบางส่วนให้ใช้

8. ตรวจสอบก่อนเปิดใช้งานจริง

  • ทดสอบการจ่าย BTC และ/หรือ EVM ยอดเล็กจนการจ่ายผู้ขายได้รับการยืนยัน ตรวจเชน สัญญาโทเคน ปลายทาง คอมมิชชัน และค่าธรรมเนียมแยกต่างหาก
  • ทดสอบ callback ซ้ำ คำตอบ API หมดเวลา การจ่ายขาด และการจ่ายที่ถูกพัก ตรวจว่าไม่มีกรณีใดทำให้ดำเนินคำสั่งซื้อหรือส่งเงินซ้ำ
  • เก็บ backup ส่วนตัวไว้นอกเซิร์ฟเวอร์ เฝ้าดูการจ่ายที่ล้มเหลว และกระทบยอดหนี้ต่อผู้ขายกับเงินจริงบนเชนเป็นประจำ

เริ่มจากอนุมัติการจ่ายด้วยตนเองและผู้ขายจำนวนน้อย เปิดอัตโนมัติเฉพาะสินทรัพย์ งบ และปลายทางที่ทดสอบแล้ว