Plaidly Docs

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 Required but no x402 challenge headers
    Treat as protocol mismatch and stop; retry once against same endpoint, then raise operator visibility.

  • create_payment_session returns non-201
    Invalid schema usually means missing or incorrect paymentMethod values (wrong methodID, unsupported token, or malformed expires_in).
    Retry only after correcting payload.

  • Session never reaches completed
    Poll until timeout (for example 90 seconds in sandbox):

    • status: "pending" → waiting on confirmation
    • status: "finalizing" → acceptable transient state
    • status: "expired" → create a new session and restart flow
  • Mismatched amount
    Treat received_amount < expected_amount as 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:

  • status must be completed.
  • 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.

On this page