Machine payments (MPP and x402, testnet)
Let an agent skip the checkout page and pay for a booking through an HTTP 402 payment link, using MPP or x402.
TestnetTestnet only: only testnet tokens are accepted, and no real money moves.
This page is for the engineers who build the integration. Merchants don't need to read it.
What machine payments are
Machine payments let an agent pay for an API or service directly, with no checkout page: the server replies with HTTP 402 and the payment requirements, the agent pays and retries with proof of payment, and gets the result and a receipt. MPP (Machine Payments Protocol) and x402 are two ways to do it, and Stripe supports stablecoin payments over both. SOP uses it for bookings: each booking awaiting payment comes with a payment link that an agent can pay with MPP or x402.
Payment flow
Steps
- Agent → SOP: POST the payment link without payment
- SOP → Agent: 402 Payment Required with MPP and x402 challenges
- Agent → Agent: Pays on the network with its wallet
- Agent → SOP: Retries with the payment credential
- SOP → SOP: Verifies the payment
- SOP → Agent: 200 with a receipt; booking marked paid
- SOP → Seller: Notifies the paid booking
The payment link
A booking awaiting payment includes a payment_link in its response: the URL, the supported protocols (x402, mpp), the amount in US dollars, the network, and testnet: true. The URL looks like /pay/{business_id}/{booking_id}?t=…; the code in it can only be used to pay, not to view, change or cancel the booking. Opened in a browser, it shows a page explaining that this is a payment link for AI agents.
A request sent without payment gets a 402 response carrying challenges for both protocols: the PAYMENT-REQUIRED header for x402, and the WWW-Authenticate: Payment header for MPP.
Machine payment: the 402 challenge (x402 and MPP, testnet)
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));
})();
Example response
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
}Run against a local copy of the service: Local copy of the connector (real Worker runtime), testnet only. request="…" is a long base64 value left out; PAYMENT-REQUIRED is shown decoded. payTo is the Stripe sandbox deposit address; the ?t= pay token is replaced by <PAY_TOKEN>.
Pay with MPP
The example below pays with pathUSD on the Tempo testnet, using the mppx command-line wallet. Once payment succeeds, the booking is marked paid (payment method mpp_tempo), becomes confirmed or waits for the merchant to confirm under its rules, and the merchant is notified.
Machine payment: pay the link with the mppx CLI (testnet)
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" }));
})();
Example response
{
"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
}
}Run against a local copy of the service: Local copy of the connector, Tempo testnet, a test wallet that holds test tokens only. The connector checks the payment on-chain; the booking becomes paid (payment_details.method mpp_tempo, reference = transaction hash). recorded_in_stripe is false because this local copy has no Stripe key.
Networks and status
| Protocol | Network and currency | Status |
|---|---|---|
| MPP | Tempo testnet (Moderato), pathUSD | Paid successfully with a test wallet in a local environment; the payment link on the live URL has not been tested yet |
| x402 | Base Sepolia, USDC | Server-side verification works; there is no successful payment on record yet (the test wallet has no test USDC) |
Testnet payment links are open only to test businesses and never appear for real businesses, so there is currently no booking available for outside parties to try.
Amount and refunds
- Amount: the booking's New Taiwan dollar amount is converted to US dollars at an exchange rate and rounded up to the cent; the rate's source and time are included in the payment link.
- Refunds: a payment that maps to a Stripe payment record is refunded in full; one that doesn't is marked "manual refund needed" (
refund: manual), and the scheduler won't refund it by mistake.
Discovery document
/openapi.json marks payable operations with x-payment-info. Each booking has a different amount, so the amount field is null.
Read the discovery document: /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"));
})();
Example response
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"
}
}
}
}
}
}Run against the live service (read-only)
What is not there yet
- Mainnet: not open; which merchants to open it to and what range of charges to allow have not been decided.
- x402: one successful testnet payment is still needed to confirm it works end to end.
Related pages
- Pay for a customer: Complete a booking payment with a credential the customer approved
- Payment credentials: Shared Payment Tokens, and the three roles: seller, agent and platform
- Protocols and standards: How MCP, REST, UCP, MPP and x402 map to SOP