Paid Service Acquisition
Acquire an x402-protected service with a Plaidly-stablecoin payment session
Paid Service Acquisition with Stablecoin + x402
This is a practical MCP-first demo for agents that need to unlock an external service behind a 402 Payment Required wall.
The runnable reference MCP server is available in plaidly-node/mcp-server.
From plaidly-node, run npm run mcp:server; for a local sandbox API, run
PLAIDLY_BASE_URL=http://127.0.0.1:8080 npm start inside mcp-server.
1) Tool surface for the agent
Keep this minimal tool manifest so the agent can perform discovery, payment, and verification:
{
"tools": [
{
"name": "create_payment_session",
"description": "Create a Plaidly stablecoin payment session that matches an x402 challenge.",
"inputSchema": {
"type": "object",
"required": ["amount", "expires_in", "paymentMethod"],
"properties": {
"amount": { "type": "number" },
"expires_in": { "type": "string" },
"metadata": { "type": "object" },
"paymentMethod": {
"type": "object",
"required": ["methodID", "chain", "token", "network"],
"properties": {
"methodID": { "type": "integer", "enum": [0] },
"chain": { "type": "string" },
"token": { "type": "string" },
"network": { "type": "string", "enum": ["testnet", "mainnet"] }
}
}
}
}
},
{
"name": "pay_x402_resource",
"description": "Call an x402-protected resource and handle challenge/proof retries.",
"inputSchema": {
"type": "object",
"required": ["url", "method"],
"properties": {
"url": { "type": "string" },
"method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"] },
"body": { "type": "string" },
"headers": { "type": "object", "additionalProperties": { "type": "string" } },
"network_hint": { "type": "string", "enum": ["testnet", "mainnet"] },
"proof": { "type": "string" }
}
}
}
]
}2) End-to-end flow (demo)
Set your environment:
export API_KEY="plaidly_sk_test_<YOUR_KEY>"
export PL_API="https://api.plaidly.io"
export SERVICE_URL="https://example-agent-service.test/agent/protected/invoice"2.1 Probe a protected endpoint
curl -i -X POST "$SERVICE_URL" \
-H 'content-type: application/json' \
-d '{"agent_task":"create_report","order_id":"ord_demo_01"}'Expected response:
HTTP/2 402
PAYMENT-REQUIRED: <base64-url-encoded-json-challenge>
X-PAYMENT-REQUIRED: <base64-url-encoded-json-challenge>Keep the decoded challenge payload for session mapping:
{
"version": "1",
"scheme": "exact",
"network": "testnet",
"maxAmountRequired": "1",
"amount": "1",
"asset": "USDC",
"payTo": "0xTestAddr...",
"recipient": "Plaidly-Agent",
"memo": "inv_ord_demo_01",
"resource": "/agent/protected/invoice",
"description": "Invoice access"
}2.2 Create a matching Plaidly session
Use a stablecoin session with methodID: 0 for on-chain transfer:
SESSION_JSON=$(cat <<'JSON'
{
"amount": 1,
"expires_in": "45m",
"metadata": {
"memo": "inv_ord_demo_01",
"resource": "/agent/protected/invoice"
},
"paymentMethod": {
"methodID": 0,
"chain": "solana",
"token": "USDC",
"network": "testnet"
}
}
JSON
)
SESSION_ID=$(curl -s -X POST "$PL_API/v1/payment_sessions" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "$SESSION_JSON" | jq -r .session_id)
echo "$SESSION_ID"Sample response (trimmed):
{
"session_id": "sess_test_01HX...",
"expected_amount": 1,
"received_amount": 0,
"currency": "USDC",
"address": "7W7...k9n",
"status": "pending",
"paymentMethod": {
"methodID": 0,
"chain": "solana",
"token": "USDC",
"network": "testnet"
}
}2.3 Simulate in sandbox (demo mode)
curl -X POST "$PL_API/v1/payment_sessions/$SESSION_ID/simulate"Then confirm completion:
curl -s "$PL_API/v1/payment_sessions/$SESSION_ID" | jq '{session_id,status,expected_amount,received_amount}'Expected result:
{
"session_id": "sess_test_01HX...",
"status": "completed",
"expected_amount": 1,
"received_amount": 1
}2.4 Pay and call the protected service
curl -i -X POST "$SERVICE_URL" \
-H "content-type: application/json" \
-H "X-Plaidly-Payment: <proof-from-pay_x402_resource>" \
-d '{"agent_task":"create_report","order_id":"ord_demo_01"}'Success when:
200 OK- response body contains requested service payload
If you are wiring this into an MCP server, pay_x402_resource can call this step automatically after generating the proof and retrying the request.
The same x402 retry helper is available in the Python SDK as
retry_request_with_x402_proof, with parse_x402_challenge and
resolve_x402_proof for lower-level control.
If you prefer explicit MCP invocation, this is the retry pattern:
{
"tool": "pay_x402_resource",
"input": {
"url": "https://example-agent-service.test/agent/protected/invoice",
"method": "POST",
"network_hint": "testnet",
"body": "{\"agent_task\":\"create_report\",\"order_id\":\"ord_demo_01\"}"
}
}Tool output on success:
{
"status": 200,
"body": "{\"invoice_id\":\"inv_ord_demo_01\",\"status\":\"issued\"}",
"headers": {
"content-type": "application/json"
}
}3) Failure handling
-
402 Payment Requiredbut no x402 challenge headers
Treat as protocol mismatch and stop; retry once against same endpoint, then raise operator visibility. -
create_payment_sessionreturns non-201
Invalid schema usually means missing or incorrectpaymentMethodvalues (wrongmethodID, unsupportedtoken, or malformedexpires_in).
Retry only after correcting payload. -
Session never reaches
completed
Poll until timeout (for example 90 seconds in sandbox):status: "pending"→ waiting on confirmationstatus: "finalizing"→ acceptable transient statestatus: "expired"→ create a new session and restart flow
-
Mismatched amount
Treatreceived_amount < expected_amountas failed payment.
4) Verify payment receipt
Proof that payment is final should include both session state and receipt material:
curl -s "$PL_API/v1/payment_sessions/$SESSION_ID" | jq '.status, .session_id, .expected_amount, .received_amount'curl -L "$PL_API/v1/payment_sessions/$SESSION_ID/receipt" \
-o "/tmp/receipt-${SESSION_ID}.pdf" \
-H "X-API-Key: $API_KEY"Quick checks:
statusmust becompleted.received_amount >= expected_amount.- saved file exists and is non-empty (
-s /tmp/receipt-${SESSION_ID}.pdf).
4.1 Event-based completion (preferred)
Listen for X-Plaidly-Event: payment_session.completed and verify signature before fulfillment logic.
See Verify webhook.
This recipe intentionally uses testnet and the demo fulfillment path to stay deterministic. Swap testnet→mainnet and replace fulfill with real on-chain payment flow for production.