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 testnetmainnet and replace fulfill with real on-chain payment flow for production.

On this page