SOPSellers' Open Protocol

機器付款(MPP與x402,測試網)

讓代理不經過結帳畫面,用HTTP 402付款連結,以MPP或x402為預約付款。

測試網目前只在測試網,只收測試網代幣,不收真實款項。

這頁是給串接的工程師看的。商家不需要讀這頁。

什麼是機器付款

機器付款讓代理不經過結帳畫面,直接為API或服務付費:伺服器回HTTP 402並附付款要求,代理付款後帶著付款證明重送,就拿到結果與收據。MPP(Machine Payments Protocol)與x402是兩種做法,Stripe都支援穩定幣收款。SOP把它用在預約:每筆等待付款的預約會附一個付款連結,代理可以用MPP或x402付款。

付款流程

機器付款:用MPP或x402支付SOP付款連結
代理SOP商家不帶付款先POST付款連結回402,附MPP與x402付款挑戰用錢包在鏈上付款帶付款證明重送驗證付款回200附收據;預約標記已付款通知已付款預約
步驟
  1. 代理 → SOP:不帶付款先POST付款連結
  2. SOP → 代理:回402,附MPP與x402付款挑戰
  3. 代理 → 代理:用錢包在鏈上付款
  4. 代理 → SOP:帶付款證明重送
  5. SOP → SOP:驗證付款
  6. SOP → 代理:回200附收據;預約標記已付款
  7. SOP → 商家:通知已付款預約

等待付款的預約,回應多一個payment_link:含網址、支援的協定(x402、mpp)、美元金額、網路與testnet: true。網址形如/pay/{business_id}/{booking_id}?t=…;其中的代碼只能用來付款,不能查看、改期或取消預約。用瀏覽器開這個網址,看到的是「這是給AI代理用的付款連結」的說明頁。

不帶付款直接送出請求,伺服器回402,同時附上兩種協定的挑戰:x402的PAYMENT-REQUIRED標頭,以及MPP的WWW-Authenticate: Payment標頭。

機器付款:402挑戰(x402與MPP,測試網)

curl

# <PAYMENT_LINK> is booking.payment_link.url from an awaiting_payment booking (testnet only).
LINK="<PAYMENT_LINK>"
curl -s -D headers.txt -o /dev/null -X POST "$LINK" -H "User-Agent: my-agent/1.0"
head -n 1 headers.txt
grep -i '^www-authenticate:' headers.txt
grep -i '^payment-required:' headers.txt | cut -d' ' -f2 | tr -d '\r' | base64 -d; echo

Python

import base64
import json
import urllib.error
import urllib.request

LINK = "<PAYMENT_LINK>"  # booking.payment_link.url from an awaiting_payment booking (testnet only)

req = urllib.request.Request(LINK, method="POST", headers={"User-Agent": "my-agent/1.0"})
try:
    urllib.request.urlopen(req)
except urllib.error.HTTPError as err:  # the 402 challenge arrives as an HTTPError
    print("HTTP", err.code)
    print("WWW-Authenticate:", err.headers["WWW-Authenticate"])  # MPP challenges (evm and tempo)
    required = json.loads(base64.b64decode(err.headers["PAYMENT-REQUIRED"]))  # x402 requirements
    print(json.dumps(required, indent=2))

Node.js

const LINK = "<PAYMENT_LINK>"; // booking.payment_link.url from an awaiting_payment booking (testnet only)

(async () => {
  const res = await fetch(LINK, { method: "POST", headers: { "User-Agent": "my-agent/1.0" } });
  console.log("HTTP", res.status);
  console.log("WWW-Authenticate:", res.headers.get("www-authenticate")); // MPP challenges (evm and tempo)
  const required = JSON.parse(Buffer.from(res.headers.get("payment-required"), "base64").toString()); // x402 requirements
  console.log(JSON.stringify(required, null, 2));
})();

回應範例

HTTP 402
WWW-Authenticate: Payment id="vRToYQ7TnWS1YWLEM_jL3lsSSJukGrQY_fqB9yVqs9g", realm="mcp.srct.tw", method="evm", intent="charge", request="…", description="… booking 88QBMCY5 (Example Studio (test data))", expires="2026-10-21T17:02:58.846Z", opaque="eyJib29raW5nX2lkIjoiODhRQk1DWTUiLCJidXNpbmVzc19pZCI6ImV4YW1wbGUtc3R1ZGlvIn0",
  Payment id="dqLCoVLnoZGkwD_-4UG_pr2KORDUXFIh1-IShyvfdKA", realm="mcp.srct.tw", method="tempo", intent="charge", request="…", description="… booking 88QBMCY5 (Example Studio (test data))", expires="2026-10-21T17:02:58.846Z", opaque="eyJib29raW5nX2lkIjoiODhRQk1DWTUiLCJidXNpbmVzc19pZCI6ImV4YW1wbGUtc3R1ZGlvIn0"
{
  "accepts": [
    {
      "amount": "18530000",
      "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "extra": {
        "assetTransferMethod": "eip3009",
        "name": "USDC",
        "version": "2"
      },
      "maxTimeoutSeconds": 300,
      "network": "eip155:84532",
      "payTo": "0x3b0b371ae3cB08F272b87319684f6518b3b313a0",
      "scheme": "exact"
    }
  ],
  "error": "Payment is required (… booking 88QBMCY5 (Example Studio (test data))).",
  "extensions": {
    "mppx": {
      "info": {
        "method": "POST",
        "opaque": "eyJib29raW5nX2lkIjoiODhRQk1DWTUiLCJidXNpbmVzc19pZCI6ImV4YW1wbGUtc3R1ZGlvIn0"
      },
      "schema": {
        "additionalProperties": false,
        "properties": {
          "_mppx_scope": {
            "type": "string"
          },
          "digest": {
            "type": "string"
          },
          "method": {
            "type": "string"
          },
          "nonce": {
            "type": "string"
          },
          "opaque": {
            "type": "string"
          }
        },
        "required": [
          "method"
        ],
        "type": "object"
      }
    }
  },
  "resource": {
    "url": "https://mcp.srct.tw/pay/example-studio/88QBMCY5?t=<PAY_TOKEN>"
  },
  "x402Version": 2
}

已對本機服務實際執行:本機的連接器副本(實際的Worker執行環境),僅限測試網。request="…"是省略的長base64值;PAYMENT-REQUIRED以解碼後呈現。payTo是Stripe沙盒的存款地址;?t=付款代碼已換成<PAY_TOKEN>。

用MPP付款

下面用mppx命令列錢包,在Tempo測試網以pathUSD付款。付款成功後,預約寫成已付款(付款方式mpp_tempo),並依商家的確認規則轉成已確認或待商家確認,再通知商家。

機器付款:用mppx命令列付款(測試網)

Shell

# One-time setup (testnet wallet): npx mppx@0.13.1 account create --account my-wallet
#                                  npx mppx@0.13.1 account fund --account my-wallet
# <PAYMENT_LINK> is booking.payment_link.url from an awaiting_payment booking (testnet only).
# --currency picks pathUSD on the Tempo testnet; without it mppx may choose the Base Sepolia USDC offer instead.
npx mppx@0.13.1 "<PAYMENT_LINK>" -X POST --account my-wallet --network testnet \
  --currency 0x20c0000000000000000000000000000000000000

Python

import subprocess

LINK = "<PAYMENT_LINK>"  # booking.payment_link.url from an awaiting_payment booking (testnet only)

# mppx answers the 402 challenge with the testnet wallet and retries the request.
result = subprocess.run(
    ["npx", "mppx@0.13.1", LINK, "-X", "POST", "--account", "my-wallet", "--network", "testnet",
     "--currency", "0x20c0000000000000000000000000000000000000"],  # pathUSD on the Tempo testnet
    capture_output=True,
    text=True,
)
print(result.stdout)
print(result.stderr)

Node.js

const LINK = "<PAYMENT_LINK>"; // booking.payment_link.url from an awaiting_payment booking (testnet only)

(async () => {
  const { execFileSync } = await import("node:child_process");
  // mppx answers the 402 challenge with the testnet wallet and retries the request.
  console.log(execFileSync("npx", ["mppx@0.13.1", LINK, "-X", "POST", "--account", "my-wallet", "--network", "testnet", "--currency", "0x20c0000000000000000000000000000000000000"], { encoding: "utf8" }));
})();

回應範例

{
  "ok": true,
  "booking": {
    "booking_id": "3K3T8RED",
    "status": "pending",
    "status_text": "Waiting for the business to confirm",
    "business_id": "example-studio",
    "business_name": "Example Studio (test data)",
    "service_id": "small_group",
    "service_name": "Small-group strength class",
    "slot": "2026-10-25T10:00",
    "start": "2026-10-25T10:00:00+08:00",
    "label": "10/25(日)10:00",
    "quantity": 1,
    "quantity_unit": "seat",
    "payment": "Paid online: NT$600",
    "details": [],
    "payment_details": {
      "mode": "online",
      "status": "paid",
      "amount": {
        "value": 60000,
        "currency": "twd",
        "text": "NT$600"
      },
      "collected_by": "Seller Tech U.S.A. LLC through Stripe",
      "paid_at": "2026-10-22T00:49:00+08:00",
      "method": "mpp_tempo",
      "reference": "0x60373d2fc9f6547ab9817f6bbe911689094159ea8dc072235b644dcf468a91c6"
    },
    "why_pending": "First booking with this business from these contact details, so the business confirms it.",
    "confirm_by": "2026-10-22T00:59:00+08:00",
    "hold": "Bookings that need the business's confirmation are held for 10 minutes. If the business does not confirm by then, the hold is released automatically and the customer can choose another slot.",
    "online_cancel_until": "2026-10-25T08:00:00+08:00"
  },
  "payment": {
    "status": "succeeded",
    "method": "mpp_tempo",
    "reference": "0x60373d2fc9f6547ab9817f6bbe911689094159ea8dc072235b644dcf468a91c6",
    "amount_usd": "18.53",
    "recorded_in_stripe": false
  }
}

已對本機服務實際執行:本機的連接器副本,Tempo測試網,錢包只有測試幣。連接器在鏈上驗證付款;預約變成已付款(payment_details.method為mpp_tempo,reference是交易雜湊)。recorded_in_stripe為false,因為這個本機副本沒有Stripe金鑰。

網路與現況

協定網路與幣別現況
MPPTempo測試網(Moderato),pathUSD已在本機環境用測試錢包付款成功;正式網址上的付款連結尚未實測
x402Base Sepolia,USDC伺服器端驗證已運作;還沒有付款成功的紀錄(測試錢包沒有測試用USDC)

測試網的付款連結只對測試用商家開放,真實商家不會出現,所以目前沒有可供外部試用的預約。

金額與退款

探索文件

/openapi.json把可付款的操作標上x-payment-info。每筆預約的金額不同,所以金額欄位是null。

讀取探索文件:/openapi.json

curl

curl -sS "https://mcp.srct.tw/openapi.json" \
  -H "User-Agent: my-agent/1.0"

Python

import json
import urllib.error
import urllib.request

BASE = "https://mcp.srct.tw"


def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        BASE + path,
        data=None if body is None else json.dumps(body).encode(),
        method=method,
        headers={"User-Agent": "my-agent/1.0", "Content-Type": "application/json", **(headers or {})},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return res.status, json.loads(res.read())
    except urllib.error.HTTPError as err:  # 4xx and 5xx responses still carry a JSON body
        return err.code, json.loads(err.read())


def show(status, body):
    print("HTTP", status)
    print(json.dumps(body, ensure_ascii=False, indent=2))


show(*call("GET", "/openapi.json"))

Node.js

const BASE = "https://mcp.srct.tw";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "User-Agent": "my-agent/1.0", "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

function show({ status, body }) {
  console.log("HTTP", status);
  console.log(JSON.stringify(body, null, 2));
}

(async () => {
  show(await call("GET", "/openapi.json"));
})();

回應範例

HTTP 200
{
  "openapi": "3.1.0",
  "info": {
    "title": "… booking payments",
    "version": "1.0.0",
    "description": "Pay for … bookings with x402 or MPP. Create the booking first with the MCP tool request_booking or POST /api/v1/businesses/{business_id}/bookings; use the payment_link it returns."
  },
  "servers": [
    {
      "url": "https://mcp.srct.tw"
    }
  ],
  "x-service-info": {
    "categories": [
      "booking",
      "fitness",
      "services"
    ],
    "docs": {
      "homepage": "https://mcp.srct.tw/",
      "apiReference": "https://mcp.srct.tw/docs"
    }
  },
  "paths": {
    "/pay/{business_id}/{booking_id}": {
      "post": {
        "summary": "Pay for a booking (x402 or MPP)",
        "parameters": [
          {
            "name": "business_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "booking_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "pay token from the booking's payment_link"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "intent": "charge",
              "method": "evm",
              "amount": null,
              "currency": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
              "description": "USDC on Base Sepolia (test network); also accepted as x402 exact"
            },
            {
              "intent": "charge",
              "method": "tempo",
              "amount": null,
              "currency": "0x20c0000000000000000000000000000000000000",
              "description": "pathUSD on Tempo test network (Moderato)"
            }
          ]
        },
        "responses": {
          "200": {
            "description": "Paid: booking and payment result"
          },
          "402": {
            "description": "Payment Required: x402 PAYMENT-REQUIRED and MPP WWW-Authenticate challenges"
          },
          "409": {
            "description": "Booking not payable"
          },
          "410": {
            "description": "Payment window passed"
          }
        }
      },
      "get": {
        "summary": "Pay for a booking (x402 or MPP)",
        "parameters": [
          {
            "name": "business_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "booking_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "t",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "pay token from the booking's payment_link"
          }
        ],
        "x-payment-info": {
          "offers": [
            {
              "intent": "charge",
              "method": "evm",
              "amount": null,
              "currency": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
              "description": "USDC on Base Sepolia (test network); also accepted as x402 exact"
            },
            {
              "intent": "charge",
              "method": "tempo",
              "amount": null,
              "currency": "0x20c0000000000000000000000000000000000000",
              "description": "pathUSD on Tempo test network (Moderato)"
            }
          ]
        },
        "responses": {
          "200": {
            "description": "Paid: booking and payment result"
          },
          "402": {
            "description": "Payment Required: x402 PAYMENT-REQUIRED and MPP WWW-Authenticate challenges"
          },
          "409": {
            "description": "Booking not payable"
          },
          "410": {
            "description": "Payment window passed"
          }
        }
      }
    }
  }
}

已對正式服務實際執行(唯讀)

還沒有的